knitr::opts_chunk$set(class.source = "code-chunks")

1 Summary

In this report I’ll give a short overview of the work done during the data science project in Programming Languages for Data Science. I worked on the whole Data Science Lifecycle from the idea to the final insights and model.

The topic is about online auctions using an online game called Guild Wars 2 as an example. The data was fetched from the Web API provided by the game developers. We talk about around 5.000.000 datasets so I had to concentrate on the most valuable items first.

The goal was to find out if there are items that can be bought low and sold high to earn (virtual) money. This was pretty much possible with data analysis and visualization. Of cause those items are changing very fast, but as a conclusion I can say it’s not possible to buy and sell “the one item”, the gains are pretty low and it’s only worth it when buying items in bulk.

As the data pretty much contains all data and doesn’t yield a real question, for the model part, I wanted to find out and predict at which rate an item can be sold. The sober consideration is that is only depends on the buying price which selling price can be achieved. All other attributes like type, rarity or required level of the item did not have any effect on the outcome.

2 Introduction

Malcolm Forbes once said, “Money isn’t everything as long as you have enough of it”. Sure, money doesn’t buy happiness, but it can buy a lot of things. This is not only true for the real world, but especially in online games, where micro transactions and payed services have become the new normal.

More and more games give you the choice to invest a lot of time or a lot of money. Both ways you’ll somehow achieve the goal of the game faster. In some games you can even exchange digital goods for real money.

To get to the point, having digital currency in games can save you time, stress and even real money.

There are multiple ways to earn money in games. One way may be farming, which means hunting special items in high amounts and selling them to other players. Here’s the question, which items are worth collecting and can be sold for which prices?

Another way is buying items from other players and reselling them with a higher price tag, just like in the real world. Many games have auction houses where those transactions can be made. For this type of income one needs to know what to buy when, which prices are low or high, and when to sell for which amount.

For this project my example of game will be Guild Wars 2. Guild Wars 2 was published in 2012 by NCSoft. There are no monthly fees and the basic game is free to play since 2015. It is a massively multiplayer online role-playing game, meaning a lot of people playing in parallel in an online world. There are over 20.000 items that can be collected and most of them can be sold and bought at the auction house, called the trading post.

The trading post

When selling items, 5% of the cost is a fee for the auction house and gets immediately taken from the wallet. Each time an item is placed in the auction house, this 5% fee must be payed, so it’s best if the items get sold on the first run.

When buying items, 10% of the price goes to the bank before the rest is delivered to the seller.

Because of that it’s important to find items where the buy-to-sell price ratio is the highest. In addition, as we buy and sell, we have to pay the whole 15% taxes which lowers our revenue.

Here is one example:

This is how it works:

  1. We want to buy an item so we create a new order with the highest order price (say: 1 gold)
  2. As soon as another player wants to sell this item, the player will sell it to us (for 1 gold)
  3. We then create a new auction for this item using the lowest offer price (say: 2 gold). We have to pay 5% for creating this auction.
  4. As soon as another player wants to buy this item, the player will buy it from us for 2 gold. During this transaction, 10% is a fee for the bank, so we get 10% less.

Overall, we invested 1.05 gold and earned 1.8 gold. This is a revenue of (only) 0.75 gold.

Coins are the basic currency

Just like in the real world, where 100 cents are 1 Euro, the main currency in Guild Wars, coins, are separated into three units. There is copper, silver and gold.

The following values are all the same and are automatically calculated by the game: ` 10.000 copper == 100 silver == 1 gold.

For this project, data is generally displayed in gold.

3 Working with the data

3.1 Data ingestion

We need to get the data first. We scrap the data from https://api.guildwars2.com/v2/commerce/. First we get all 26800 items and, in batches of 200 items per request, get all auctions for these items. It’s really important to use the batch endpoint to keep the runtime to a reasonable duration.

dir <- getwd()
date <- params$data_date

price_list_buys <- read.csv(paste(dir, "/../data/raw/gw2-all-buys-raw-", date, ".csv", sep = ""))
price_list_sells <- read.csv(paste(dir, "/../data/raw/gw2-all-sells-raw-", date, ".csv", sep = ""))
item_list <- read.csv(paste(dir, "/../data/raw/gw2-all-items-raw-", date, ".csv", sep = ""))

df_buys <- item_list %>% left_join(price_list_buys, by = "id")
df_sells <- item_list %>% left_join(price_list_sells, by = "id")

rm(dir)
rm(date)
rm(item_list)
rm(price_list_sells)
rm(price_list_buys)

First we clean the data and change some feature types. As already said, most of the time we use the price in gold, so we add this feature.

df_sells <- df_sells %>% 
  drop_na(unit_price, quantity) %>% 
  mutate(rarity = as.factor(rarity),
         type = as.factor(type),
         item_type = as.factor(item_type),
         item_weight_class = as.factor(item_weight_class),
         unit_price_gold = unit_price / 10000) %>% 
  select(-unit_price)

df_buys <- df_buys %>% 
  drop_na(unit_price, quantity) %>% 
  mutate(rarity = as.factor(rarity),
         type = as.factor(type),
         item_type = as.factor(item_type),
         item_weight_class = as.factor(item_weight_class),
         unit_price_gold = unit_price / 10000) %>% 
  select(-unit_price)

After acquiring the data this way we have one file with all the items, one with all the sell orders (3855399) and one with all the buy orders (around 434399). When joining those files together, the amount of data would be enormous.

3.1.1 Filter on highest buy and lowest sell orders

Therefore some filtering is required first. We only take the highest buy orders and the lowest sell orders, because these are the first ones to be bought/sold.

df_max_buys <-  df_buys %>% 
  group_by(name) %>% 
  slice(which.max(unit_price_gold))

df_min_sells <-  df_sells %>% 
  group_by(name) %>% 
  slice(which.min(unit_price_gold))

Now we can join the sell and buy orders and calculate the profit per item according to the tax rules described earlier.

df_all <- df_max_buys %>%
  mutate(quantity_buys = quantity,
         unit_price_gold_buys = unit_price_gold) %>% 
  select(-quantity, -unit_price_gold) %>% 
  right_join(df_min_sells %>% 
              mutate(quantity_sells = quantity,
                     unit_price_gold_sells = unit_price_gold) %>% 
              select(id, quantity_sells, unit_price_gold_sells), by = "id") %>% 
  mutate(name = name.x) %>% 
  select(-name.x, -name.y)
df_all <- df_all %>% 
  mutate(unit_price_gold_diff = unit_price_gold_sells - unit_price_gold_buys,
         profit = 0.85 * unit_price_gold_sells - unit_price_gold_buys,
         more_sells = quantity_sells - quantity_buys)

As we can see there are some very high outliers and a lot of outliers between around 100 and 3000 gold. We need to zoom in much more to see the details.

df_all %>% 
ggplot() +  
  geom_boxplot(aes(x = 'Sells', y = unit_price_gold_sells)) +
  geom_boxplot(aes(x = 'Buys', y = unit_price_gold_buys)) +
  geom_boxplot(aes(x = 'Profit', y = profit)) +
  geom_hline(yintercept = 3000, linetype="dashed", color = "red") +
  geom_hline(yintercept = 100, linetype="dashed", color = "blue") +
  scale_y_continuous(labels = comma) +
  labs(title = "Outliers on buys and sells", subtitle = "Baseline between 100 and 3000 gold",
    x = "", y = "Price in gold", caption = paste("Data from", params$data_date))
## Warning: Removed 3756 rows containing non-finite values (stat_boxplot).
## Removed 3756 rows containing non-finite values (stat_boxplot).

df_all %>% 
  subset(profit < 2.5 & unit_price_gold_sells < 2.5) %>% 
ggplot() +  
  geom_boxplot(aes(x = 'Sells', y = unit_price_gold_sells)) +
  geom_text(aes(x = 'Sells', y = median(unit_price_gold_sells), label = median(unit_price_gold_sells)), size = 3, vjust = -1) +
  geom_boxplot(aes(x = 'Profit', y = profit)) +
  geom_text(aes(x = 'Profit', y = median(profit), label = median(profit)), size = 3, vjust = -0.5) +
  scale_y_continuous(labels = comma) +
  labs(title = "Outliers on profit and sells", subtitle = "Limit at 2.5 gold profit and sell price",
    x = "", y = "Price in gold", caption = paste("Data from", params$data_date))

median_profit_silver <- round(100 * df_all %>% 
  drop_na(profit) %>% 
  summarise(median(profit)) %>% 
  first(), 2)

median_profit_silver_filter <- round(100 * df_all %>% 
  subset(profit < 2.5 & unit_price_gold_sells < 2.5) %>% 
  drop_na(profit) %>% 
  summarise(median(profit)) %>% 
  first(), 2)

The median profit is not in the gold range, but at 14.37 silver. When filtering with reasonable bounds, the profit even falls down to 4.11 silver.

3.1.2 Filter on realistic profits

We can now strip down the data even further.

Let’s take only the items with a realistic profit and strip away the items where it would be better to sell them to the non-player vendor instead of placing them in the auction house.

df_all <- df_all %>% 
  subset(profit > 0.04 & profit < 0.4)

df_all <- df_all %>% 
  subset(profit * 100 > vendor_value) %>% 
  arrange(desc(profit))

Now only 293 items left. Let’s have a closer look at those items.

df_all %>%
  group_by(type, rarity) %>% 
  summarise(name = unique(name),
            profit = profit * 100,
            icon = min(web_image(icon, height = 50))
            ) %>% 
  drop_na() %>% 
  arrange(desc(profit)) %>%
  gt(rowname_col = "name") %>%
  tab_header(title = "Realistic top profit items", subtitle = "") %>%
  fmt_number(
    columns = profit,
    suffixing = "S"
  ) %>% 
  fmt_markdown(
    columns = icon
  ) %>% 
  summary_rows(
    groups = TRUE,
    columns = profit,
    fns = list(average = "mean"),
    formatter = fmt_number
  ) %>%
  tab_footnote(
    footnote = "Prices in silver",
    locations = cells_column_labels(columns = profit)
  ) %>%
  tab_source_note(
    "Based on data from api.guildwars2.com"
  ) %>% 
  tab_options(
    summary_row.background.color = "#ACEACE",
    row_group.background.color = "#FFEFDB",
    table.layout = "auto",
    container.overflow.x = TRUE,
    container.height = px(350)
  )
Realistic top profit items
profit1 icon
CraftingMaterial - Basic
Oiled Orichalcum Sword Hilt 39.98
Oiled Orichalcum Horn 39.65
Warbeast Orichalcum Pauldron Casing 38.33
Oiled Small Ancient Haft 38.11
Warbeast Hardened Helmet Strap 35.07
Rugged Longcoat Panel 34.98
Warbeast Gossamer Pant Lining 34.69
Oiled Orichalcum Shield Backing 34.56
Warbeast Orichalcum Gauntlet Plates 31.93
Oiled Orichalcum Boot Casing 31.05
Warbeast Hardened Shoulderguard Padding 29.61
Coarse Longcoat Panel 28.67
Hardened Boot Upper 24.58
Steel Splint Chestplate Panel 20.91
Wool Footwear Upper 20.34
Wool Vestments Panel 20.02
Rugged Trouser Panel 18.87
Elonian Boot Sole 18.30
Iron Spear Head 18.20
Oiled Orichalcum Sword Blade 17.49
Elonian Trouser Padding 17.44
Darksteel Legging Panel 17.18
Orichalcum Chestplate Panel 16.73
Rugged Glove Panel 15.56
Darksteel Pistol Barrel 14.39
Oiled Ancient Rifle Stock 13.75
Green Harpoon 13.52
Gossamer Coat Panel 13.39
Thick Longcoat Panel 13.33
Iron Scale Legging Panel 13.20
Rawhide Chestguard Panel 13.09
Iron Mace Head 12.94
Iron Scale Armguard Panel 12.68
Ancient Longbow Stave 12.51
Iron Axe Blade 12.46
Bronze Mace Head 12.01
Orichalcum Dagger Blade 11.58
Gossamer Epaulet Panel 11.46
Coarse Trouser Panel 11.34
Steel Sword Blade 11.10
Rawhide Legging Panel 10.73
Soft Harpoon 10.57
Orichalcum Chain 10.57
Steel Pistol Barrel 10.40
Hardened Shoulderguard Panel 10.39
Gossamer Gloves Panel 10.08
Iron Shield Backing 10.04
Gossamer Helm Strap 10.02
Simple Tailor's Tools 10.02
Steel Rifle Barrel 9.91
Steel Hammer Head 9.90
Ancient Short-Bow Stave 9.88
Linen Coat Panel 9.88
Hard Scepter Rod 9.80
Hardened Boot Sole 9.78
Orichalcum Sword Hilt 9.66
Oiled Hardened String 9.50
Orichalcum Spear Head 9.42
Thin Legging Panel 9.32
Linen Helm Strap 9.18
Steel Splint Pauldron Casing 8.76
Iron Scale Legging Lining 8.67
Iron Scale Chest Panel 8.61
Iron Pauldron Casing 8.49
Hardened Helmet Strap 8.18
Orichalcum Legging Panel 8.11
Bolt of Embroidered Silk 8.04
Steel Splint Boot Casing 7.96
Seasoned Staff Shaft 7.51
Steel Splint Gauntlet Plates 7.49
Steel Mace Head 7.48
Darksteel Horn 7.42
Rugged Boot Upper 7.34
Green Staff Shaft 7.30
Steel Shield Boss 7.27
Hardened Glove Panel 7.11
Bronze Greatsword Hilt 7.05
Darksteel Pauldron Casing 6.73
Steel Shield Backing 6.63
Hard Harpoon 6.30
Simple Huntsman's Tools 6.28
Iron Scale Boot Panel 6.27
Rugged Shoulderguard Panel 6.24
Steel Dagger Blade 6.21
Jute Breeches Panel 6.13
Bronze Dagger Blade 6.11
Steel Spear Head 5.93
Wool Headpiece Strap 5.80
Iron Sword Hilt 5.75
Bronze Torch Head 5.62
Bronze Chain Chest Panel 5.54
Tub of Wood Glue 5.49
Iron Casque Lining 5.45
Green Short-Bow Stave 5.45
Iron Hammer Head 5.43
Bowl of Cream Soup Base 5.41
Iron Rifle Barrel 5.26
Steel Torch Head 5.25
Small Hard Haft 5.22
Seasoned Rifle Stock 5.16
Iron Scale Chest Padding 5.11
Steel Trident Head 5.04
Iron Horn 4.96
Green Scepter Rod 4.88
Wool Epaulet Padding 4.85
Pile of Simple Chili Seasoning 4.72
Thin Glove Lining 4.45
Bronze Spear Head 4.08
average 12.49
CraftingMaterial - Exotic
Mordant Inscription 39.95
Inscription of the Spearmarshal 36.90
Smell-Enhancing Culture 28.17
Insignia of the Spearmarshal 26.13
Insignia of the Harrier 15.34
average 29.30
Consumable - Fine
Recipe: Potent Master Tuning Crystals 39.92
Recipe: Bowl of Refugee's Beet Soup 39.92
Writ of Studied Strength 39.57
Bowl of Cactus Fruit Salad 38.24
Recipe: Toxic Focusing Crystal 38.19
Thesis on Studied Accuracy 35.28
Prickly Pear Stuffed Nopal 33.65
Thesis on Calculated Accuracy 30.75
Writ of Accuracy 28.09
Writ of Basic Speed 27.57
Writ of Calculated Malice 26.84
Thesis on Basic Accuracy 25.17
Recipe: Bowl of Zesty Turnip Soup 19.46
Cheese Pizza 18.58
Writ of Basic Strength 13.16
Writ of Basic Accuracy 12.77
Minor Potion of Destroyer Slaying 11.64
Thesis on Basic Malice 11.53
Bowl of Candy Corn Ice Cream 10.44
Eggs Beetletun 7.16
Canopy Dye 6.57
Writ of Basic Malice 5.88
Potion of Flame Legion Slaying 5.68
Thesis on Basic Strength 5.60
Potion of Undead Slaying 4.61
average 21.45
Consumable - Rare
Grave Dye 39.86
White Tiger Staff Skin 39.57
Bloodstone Dark Coral 39.24
Rogue Grymm Svaard 38.15
Arcane Dye 38.15
Improvised Dagger Skin 35.02
Blue Orchid Dye 34.98
Glacial Focus Skin 34.31
Bloodstone Dark Indigo 33.35
Mesa Dye 31.56
Daybreak Dye 31.23
White Tiger Axe Skin 29.63
Toxin Dye 29.00
Recipe: 20-Slot Equipment Pact Box 28.93
Ruin Dye 28.32
Bloodstone Indigo 27.61
Gargoyle Shield Skin 27.47
Draconic Rifle Skin 26.90
Shadow Sword Skin 26.44
Amenity Dye 24.85
Perseverance Dye 24.74
White Tiger Hammer Skin 24.67
Gargoyle Longbow Skin 24.62
Equinox Focus Skin 24.12
Crushed Bone Dye 23.71
White Tiger Shield Skin 22.67
Red Crane Short Bow Skin 22.53
Auric Dye 21.52
Mystic Forge Node 21.14
Sunfire Lava Dye 19.99
Carnage Orange Dye 18.41
Steampunk Tybalt 18.24
Blue Steel Dye 16.96
Draconic Longbow Skin 16.68
Blacklight Dye 16.28
Shiver Sky Dye 15.26
Tar Dye 15.13
Benevolence Dye 15.04
Gallant Warhorn Skin 14.96
Wind Catcher Skin 14.49
Phalanx Turai Ossa 13.80
Gallant Focus Skin 13.39
Bloodstone Violet 12.03
Priory Grymm Svaard 11.22
Swampblack Dye 11.11
Frozen Scales Dye 11.03
Dragon's Jade Warhammer Skin 9.54
Shadow Rifle Skin 8.69
Quartz Dye 8.58
Abyss Stalker Dagger Skin 6.36
War God's Focus 5.88
Draconic Dagger Skin 5.69
Limonite Dye 5.65
average 21.86
Consumable - Masterwork
Great Guild Firework 39.52
Feast of Sage-Stuffed Poultry 39.41
Tray of Chocolate Cherries 37.47
Feast of Bean Salad 37.09
Bowl of Passion Fruit Tapioca Pudding 36.23
Feast of Garlic Spinach Sautee 36.22
Tray of Garlic Bread 34.13
Feast of Pepper Steak Dinners 32.45
Winter Warband Festive Mortar 31.66
Tray of Chocolate Bananas 27.54
Pineapple Dye 24.63
Mystery Quaggan Tonic 21.66
Tray of Chocolate Oranges 17.44
Watermelon Dye 12.90
Mummy Tonic 11.90
Violet Tint Dye 10.06
Plate of Eggs Benedict 7.99
Lunar New Year Firework 5.49
Parrot Mail Carrier 5.01
Raven Mail Carrier 4.39
average 23.66
Weapon - Fine
Half Eaten Scepter 37.86
Half Eaten Longbow 29.98
Half Eaten Hammer 23.31
Asuran Harpoon 20.72
Copper Mace 20.10
Half Eaten Rifle 20.08
Half Eaten Torch 16.02
Half-Eaten Short Bow 12.18
Half Eaten Shield 8.20
average 20.94
Container - Basic
Small Bag of Skritt Shinies 36.34
Deciphered Clues 30.77
Bag of Laboratory Materials 26.94
Light Icy Bag 15.94
average 27.50
CraftingMaterial - Rare
Honed Intricate Cotton Insignia 35.85
Strong Steel Imbued Inscription 35.03
Vigorous Iron Imbued Inscription 31.61
Tengu Echo Blade 28.57
average 32.77
CraftingMaterial - Masterwork
Giver's Embroidered Silk Insignia 35.26
Practical Leatherworker's Tools 26.87
Bag of Radiant Energy 24.10
Bag of Incandescent Energy 18.64
Bag of Dolyak Chow 16.18
Steel Reinforcing Plate 10.27
Bag of Shimmering Energy 10.17
Copper Reinforcing Plate 7.47
average 18.62
Trophy - Rare
Giant Mushroom Spore 33.63
Coastal Lumber Core 18.05
Lamp Finial 10.22
Dollop of Choya Harissa 8.30
Visage of Dwayna 7.43
Ash Legion Key 5.93
Jungle Grass Seed 5.15
Palm Lumber Core 4.31
average 11.63
Bag - Fine
15 Slot Invisible Pack 32.66
15 Slot Invisible Bag 28.98
12 Slot Craftsman's Bag 28.10
15 Slot Oiled Pack 27.27
average 29.25
Container - Fine
Box of Simple Mighty Chain Armor 30.32
average 30.32
Trophy - Masterwork
Amber Quantic Dipole 28.95
average 28.95
CraftingMaterial - Fine
Sturdy Tailor's Tools 27.08
Sturdy Weaponsmith's Tools 8.46
Sturdy Leatherworker's Tools 8.30
average 14.61
UpgradeComponent - Masterwork
Minor Rune of the Adventurer 26.26
Minor Rune of the Brawler 23.28
Minor Rune of the Scholar 21.81
Minor Rune of the Mesmer 18.59
Minor Rune of Perplexity 12.24
Minor Rune of the Warrior 10.00
Minor Rune of the Guardian 9.20
Minor Rune of the Thief 7.67
Minor Rune of the Ogre 6.83
Minor Rune of Altruism 6.12
Minor Rune of the Elementalist 5.08
average 13.37
MiniPet - Exotic
Mini GL-XC S7S 26.21
Mini Charles the Hellfire Skeleton 10.18
average 18.19
Gizmo - Masterwork
Masterwork Black Lion Dye Canister—Yellow 24.72
Masterwork Black Lion Dye Canister—Red 16.82
Endless Common Clothing Tonic 4.20
average 15.25
Consumable - Basic
Offering Basket 23.39
Small Dusty Wintersday Gift 15.18
average 19.28
Trophy - Basic
Skritt Artifact 16.81
Hylek Armor 7.03
average 11.92
Gizmo - Basic
Capacitive Bottle 16.71
Guild Siege Golem Blueprints 14.84
Guild Shield Generator Blueprint 13.05
Guild Flame Ram Blueprint 7.30
Guild Trebuchet Blueprints 6.31
average 11.64
Container - Exotic
Unopened Endless Branded Tonic 14.50
average 14.50
Weapon - Exotic
Staff of the Lost of Severance 14.25
Might of the Vindictive of Severance 14.09
Forged Bow 13.21
Gate of Good-Byes of Severance 10.52
average 13.02
Trophy - Exotic
Pile of Jacarandere 13.58
average 13.58
Trophy - Ascended
Heat Stone 12.94
average 12.94
Container - Masterwork
Unopened Crystal Shard Kite 12.77
average 12.77
Gizmo - Exotic
Infinite Molten Berserker Tonic 8.77
Endless Halloween Tonic 7.37
average 8.07
Consumable - Exotic
Visage of the Great Ram Firework 7.64
Visage of the Great Monkey Firework 6.62
Recipes of the Dwarven Leather Trader 4.43
Twisted Watchwork Portal Device 4.32
average 5.75
Armor - Fine
Darkvine Cloth Gloves 7.26
average 7.26
Based on data from api.guildwars2.com
1 Prices in silver

3.2 Data splitting

To make the data split reproducible, we set a seed. We want to predict the gold price based on attributes of the items.

set.seed(42)

# Put 3/4 of the data into the training set 
data_split <- initial_split(df_all, 
                           prop = 3/4, 
                           strata = profit, 
                           breaks = 4)

# Create dataframes for the two sets:
train_data <- training(data_split) 
test_data <- testing(data_split)

We also create a validation set using the CV_fold method that is used during modeling.

3.2.1 >TODO: use cs_folds somewhere ??

cv_folds <- 
  vfold_cv(train_data,
           v=5,
           strata = profit,
           breaks = 4)

df_train <- train_data 

From now on we will work with the train data (df_train).

3.3 Analyze data

3.3.1 Find correlations

df_train %>% 
  select(where(is.numeric), -more_sells, -profit, -unit_price_gold_diff) %>% # only select numerical data
  vis_cor(cor_method = "spearman", na_action = "pairwise.complete.obs")

We see that our data is pretty uncorrelated which makes it hard to find a good classification model. What we see is that buy and sell price are somewhat correlated as well as the level of the item to the vendor value.

The latter makes sense, as there must be some kind of algorithm that sets the sell price, probably also based on the item’s level.

Following idea: A model that predicts the sell price based on the buy price. Then we search for outliers where the sell price was predicted too high. Probably those items are underrated somehow?

3.3.2 Price distribution

Let’s have a look at the distribution of profit in general for these items. We can see that the types are distributed pretty evenly within our small profit range.

df_train_distribution <- df_train %>% 
  group_by(name) %>% 
  summarise(mean_profit = mean(profit),
            type = unique(type),
            rarity = unique(rarity)) %>% 
  arrange(desc(mean_profit))
  
df_train_distribution %>% 
  ggplot() +
  geom_histogram(aes(x =  mean_profit, fill = type), stat="count") +
  scale_x_binned(limits = c(0, 0.4)) +
  labs(x = "Mean profit", y = "Count",
       title = "Item profit distribution", subtitle = "Items by profit, in gold", 
       caption = paste("Data from", params$data_date))
## Warning: Ignoring unknown parameters: binwidth, bins, pad

3.3.3 Cluster Analysis

To get a better overview how the item buy costs and profits relate to each other, we’ll do a cluster analysis.

(taken from https://www.kirenz.com/post/2020-05-21-r-hierarchische-clusteranalyse/)

df_cl <- df_train %>% 
  select(c("id", "profit", "unit_price_gold_buys"))

df_cl$profit <- scale(df_cl$profit, center = TRUE, scale = TRUE)
df_cl$unit_price_gold_buys <- scale(df_cl$unit_price_gold_buys, center = TRUE, scale = TRUE)
d <- 
  df_cl %>% 
  select(-id) %>% 
  dist(method = "euclidean")

hc <- hclust(d, method = "ward.D2") 
plot(hc) 

The dendrogram displays the number of clusters. The higher the number, the less similar are the clusters to each other. Because there is a big gap between ~ 5 and ~ 10 which would result in only two clusters, four clusters seem fine.

We can also try six clusters, as two of the clusters seem to be pretty small.

hc$labels <- df_cl$id

grp <- cutree(hc, k = 4) 
df_cl$cluster <- grp

df_cl %>% 
  ggplot(aes(unit_price_gold_buys, 
             profit, 
             color = factor(cluster))) +
  geom_point() +
  # geom_text(aes(label = id), size = 3, check_overlap = FALSE, vjust = 0, nudge_y = 0.1) +
  xlab("Buy Costs") +
  ylab("Profit") +
  theme(legend.title=element_blank())

rm(d)
rm(hc)
rm(grp)

Because we scaled the numbers we can’t really say something about the real value of the items, but most of them are in the lower cost / lower profit range.

Four clusters were created out of the data:

  • The “(1) red” cluster are low-cost items with low profit (don’t buys)
  • The “(2) green” cluster are high-cost items with a high variance in profit (maybe more outliers)
  • The “(3) teal” cluster are mid-cost items with different profit
  • The “(4) purple” cluster are low-cost items with high profit (should buys)

The profit is not a question of buy costs, there are items with high profit for low and for high costs.

There are some items that are cheap but will bring good profit.

3.3.4 Find profitable items

Starting with a low budget, we can’t buy a lot of high-priced items. Therefore it’s good to know which items get the most profit compared to their costs. We always suppose the worst profit.

df_train_high_roi <-  df_train %>% 
  group_by(name) %>% 
  summarise(
    id = unique(id),
    profitByCost = min(profit) / max(unit_price_gold_buys),
    profit = min(profit),
    cost = max(unit_price_gold_buys),
    sell = min(unit_price_gold_sells),
    quantity = min(quantity_buys)
  ) %>% 
  ungroup() %>% 
  arrange(desc(profitByCost))
df_train_high_roi %>% 
  ggplot(aes(x = cost,  y = profit)) +
  geom_point() +
  geom_smooth(method='lm', formula= y~x) +
  labs(x = "Cost", y = "Profit",
       title = "Cost and Profit of Items", subtitle = "Prices in gold",
       caption = paste("Data from", params$data_date))

This is basically the same picture we saw earlier on cluster analysis, but with the real profit values.

Let’s have a closer look at those items. Because the prices are in the lower silver range again, they are displayed as silver here:

df_train_high_roi %>% 
  mutate(
    profitByCost = round(profitByCost, digits = 2),
    profit = round(profit * 100, digits = 2),
    cost = round(cost * 100, digits = 2),
    sell = round(sell * 100, digits = 2)
    ) %>% 
  filter(profit > 0) %>% 
  select(name, profitByCost, profit, cost, sell, quantity) %>% 
  datatable(extensions = c('ColReorder', 'Buttons', 'Responsive', 'Scroller', 'SearchPanes', 'Select'),
            options = list(colReorder = TRUE,
                           dom = 'Bfrtip', 
                           buttons = list('searchPanes', 'copy', list(
                                           extend = 'collection',
                                           buttons = c('csv', 'excel', 'pdf'), 
                                           text = 'Download'))),
            escape = FALSE,
            colnames=c("Name", "Profit by cost", "Profit", "Buy costs", "Sell revenue", "Quantity")
            )

Great, we found items with very high profit. Let’s buy some of them that also have a high quantity.

3.4 Model

3.4.1 Feature Selection

As we saw while analyzing the data, the features are uncorrelated except buy and sell prices.

After testing, the prediction is even better when we take only the unit_price_gold_buys feature into account in opposite to taking more features into account like type, rarity or level. To make this clear, we first take all features and look for correlations.

df_train <- train_data %>% 
  select(id, name, unit_price_gold_sells, unit_price_gold_buys, type, rarity, level) %>% 
  drop_na()
sells_rec <- 
  recipe(unit_price_gold_sells ~ ., data = df_train) %>% 
  update_role(id, name, new_role = "ID") %>% 
  step_dummy(all_nominal_predictors())%>% 
  step_zv(all_predictors()) %>%  # remove zero vectors
  step_center(all_predictors()) %>%
  step_scale(all_predictors())

summary(sells_rec)

We now have four predictors and one outcome. The other fields are for reference if we want to dig deeper into the results of the model.

The recipe creates dummy variables for all nominal predictors, this is useful for type and rarity.

zv would removes all n/a vectors which would be helpful for item_type and item_weight_class as these values are not present for all rows, but we don’t use these features.

Finally the unit_price_gold_buys will be centered and scaled to better work with the algorithm.

3.4.2 Training

We use the random forest regression model to train with the ranger engine, and the linear regression model using the glmnet engine. In opposite to the glm engine, glmnet only supports inputs with more than one predictor.

# parsnip model
set.seed(42)

rf_mod <- 
  rand_forest() %>% 
  set_engine("ranger") %>% 
  set_mode("regression")

lasso_mod <- 
  linear_reg(penalty = 0.1, mixture = 1) %>% 
  set_engine("glmnet")

sells_wflow <- 
  workflow() %>% 
  add_recipe(sells_rec) 

rf_wflow <- 
  sells_wflow %>% 
  add_model(rf_mod)


lasso_wflow <- 
  sells_wflow %>% 
  add_model(lasso_mod)

rf_wflow
## == Workflow ====================================================================
## Preprocessor: Recipe
## Model: rand_forest()
## 
## -- Preprocessor ----------------------------------------------------------------
## 4 Recipe Steps
## 
## * step_dummy()
## * step_zv()
## * step_center()
## * step_scale()
## 
## -- Model -----------------------------------------------------------------------
## Random Forest Model Specification (regression)
## 
## Computational engine: ranger
lasso_wflow
## == Workflow ====================================================================
## Preprocessor: Recipe
## Model: linear_reg()
## 
## -- Preprocessor ----------------------------------------------------------------
## 4 Recipe Steps
## 
## * step_dummy()
## * step_zv()
## * step_center()
## * step_scale()
## 
## -- Model -----------------------------------------------------------------------
## Linear Regression Model Specification (regression)
## 
## Main Arguments:
##   penalty = 0.1
##   mixture = 1
## 
## Computational engine: glmnet

3.4.3 Evaluation

We can now fit the train data and check the results with our two different models.

One for random forest:

sells_fit <- 
  rf_wflow %>% 
  fit(data = df_train)

sells_rf_aug <- 
  augment(sells_fit, test_data)

sells_rf_aug %>% 
  select(name, unit_price_gold_sells, .pred)
sells_rf_aug %>% 
  metrics(truth = unit_price_gold_sells, estimate = .pred)

One for lasso:

sells_fit <- 
  lasso_wflow %>% 
  fit(data = df_train)

sells_lasso_aug <- 
  augment(sells_fit, test_data)

sells_lasso_aug %>% 
  select(name, unit_price_gold_sells, .pred)
sells_lasso_aug %>% 
  metrics(truth = unit_price_gold_sells, estimate = .pred)

Comparing the errors of those two engines, the random forest algorithm has a much higher root mean square error and mean absolute error. Both models are overwhelmingly exact, random forest on 94% of the time and lasso on 99,988%.

sells_fit %>%
  extract_fit_parsnip() %>%
  tidy()
## Lade nötiges Paket: Matrix
## 
## Attache Paket: 'Matrix'
## Die folgenden Objekte sind maskiert von 'package:tidyr':
## 
##     expand, pack, unpack
## Loaded glmnet 4.1-4

Now we have it in numbers that the only feature that has influence on our prediction is the buying price. Maybe we can change this using hyper parameter tuning.

3.4.4 Tuning

set.seed(1234)

sells_boot <- bootstraps(df_train)

tune_spec <- linear_reg(penalty = tune(), mixture = 1) %>%
  set_engine("glmnet")

lambda_grid <- grid_regular(penalty(), levels = 50)
doParallel::registerDoParallel()

set.seed(2020)

lasso_grid <- 
  tune_grid(
  sells_wflow %>% 
    add_model(tune_spec),
    resamples = sells_boot,
    grid = lambda_grid
)

lasso_grid %>%
  collect_metrics()
lasso_grid %>%
  collect_metrics() %>%
  ggplot(aes(penalty, mean, color = .metric)) +
  geom_errorbar(aes(ymin = mean - std_err,
                    ymax = mean + std_err),
                alpha = 0.5) +
  geom_line(size = 1.5) +
  facet_wrap(~.metric, scales = "free", nrow = 2) +
  scale_x_log10() +
  theme(legend.position = "none")

Well bad luck, nothing to gain here. The rmse even raises after too much tuning.

Nonetheless, the error values are already pretty low.

sells_join_aug <- sells_lasso_aug %>% 
  select(id = id, lasso_pred = .pred, unit_price_gold_sells) %>% 
  left_join(sells_rf_aug %>% select(id = id, rf_pred = .pred), by = "id")

sells_join_aug %>%  
  ggplot() +
  geom_point(aes(x = rf_pred, y = unit_price_gold_sells), color = "#05541a") +
  geom_point(aes(x = lasso_pred, y = unit_price_gold_sells), color = "#040552") +
  geom_abline(col = "red", lty = 2) +
  labs(x = "Prediction", y = "Gold value",
       title = "Item price predictions", subtitle = "Using random forest and lasso regressions",
       color = c("A", "B"),
       caption = paste("Data from", params$data_date))

What is interesting for us are the prices that are blow the prediction line, meaning that there may be a current low that will probably change in the future.

3.4.5 Evaluate on test set

Do the last fit on the test data. We changed no parameter so the output should be the same as before.

# final evaluation with test data
last_fit_lasso <- last_fit(lasso_wflow, split = data_split)

# show RMSE and RSQ
last_fit_lasso %>% 
  collect_metrics()
LS0tDQp0aXRsZTogIkluc2lnaHRzIGluIHRoZSBlY29ub215IG9mIGFuIE1hc3NpdmVseSBNdWx0aXBsYXllciBPbmxpbmUgUm9sZS1QbGF5aW5nIEdhbWUiDQphdXRob3I6ICJMdWthcyBaYWlzZXIiDQpwYXJhbXM6IA0KIGluc3RpdHV0ZTogIkhkTSBTdHV0dGdhcnQiDQogY291bnRyeTogIkdlcm1hbnkiDQogeWVhcjogIjIwMjIiDQogZGF0YV9kYXRlOiAiMjAyMi0wNC0wMSINCm91dHB1dDoNCiBodG1sX2RvY3VtZW50OiANCiAgY3NzOiBzdHlsZS5jc3MgIyBkZWZpbmUgeW91ciBvd24gY3NzDQogIGRmX3ByaW50OiBwYWdlZCAjICB0YWJsZXMgYXJlIHByaW50ZWQgYXMgSFRNTCB0YWJsZXMgDQogIGhpZ2hsaWdodDogZGVmYXVsdCAjIHN5bnRheCBoaWdobGlnaHRpbmcgc3R5bGUgDQogIG51bWJlcl9zZWN0aW9uczogeWVzICMgbnVtYmVyaW5nIG9mIHNlY3Rpb25zDQogIHRoZW1lOiBwYXBlciAjIHN0eWxlIG9wdGlvbg0KICBmaWdfaGVpZ2h0OiA0ICMgZmlndXJlIGhlaWdodA0KICBmaWdfd2lkdGg6IDggIyBmaWd1cmUgd2lkdGgNCiAgZmlnX2NhcHRpb246IGZhbHNlDQogIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgdG9jOiB5ZXMgIyB0YWJsZSBvZiBjb250ZW50DQogIHRvY19mbG9hdDogDQogICAgY29sbGFwc2VkOiB0cnVlICMgc2hvdyBmdWxsIHRvYw0KICAgIHNtb290aF9zY3JvbGw6IHRydWUgIyB0b2Mgc2Nyb2xsaW5nIGJlaGF2aW9yDQogIGluY2x1ZGVzOg0KICAgIGFmdGVyX2JvZHk6IGZvb3Rlci5odG1sICMgaW5jbHVkZSBmb290ZXINCi0tLQ0KDQpgYGB7ciBpbml0LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBpbmNsdWRlPUZBTFNFfQ0KIyBCYXNpY3MNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShyZWFkcikNCmxpYnJhcnkodmlzZGF0KQ0KDQojIFBsb3R0aW5nDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KGd0KQ0KbGlicmFyeShndGFibGUpDQpsaWJyYXJ5KERUKQ0KDQojIFdvcmtpbmcgd2l0aCBtb2RlbHMNCmxpYnJhcnkocnNhbXBsZSkNCmxpYnJhcnkodGlkeW1vZGVscykNCmxpYnJhcnkoR0dhbGx5KQ0KbGlicmFyeShjb3JycikNCmxpYnJhcnkocGFyc25pcCkNCmxpYnJhcnkoeWFyZHN0aWNrKQ0KYGBgDQoNCmBgYHtyfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGNsYXNzLnNvdXJjZSA9ICJjb2RlLWNodW5rcyIpDQpgYGANCg0KDQojIFN1bW1hcnkNCg0KSW4gdGhpcyByZXBvcnQgSSdsbCBnaXZlIGEgc2hvcnQgb3ZlcnZpZXcgb2YgdGhlIHdvcmsgZG9uZSBkdXJpbmcgdGhlIGRhdGEgc2NpZW5jZSBwcm9qZWN0IGluIGBQcm9ncmFtbWluZyBMYW5ndWFnZXMgZm9yIERhdGEgU2NpZW5jZWAuDQpJIHdvcmtlZCBvbiB0aGUgd2hvbGUgKipEYXRhIFNjaWVuY2UgTGlmZWN5Y2xlKiogZnJvbSB0aGUgaWRlYSB0byB0aGUgZmluYWwgaW5zaWdodHMgYW5kIG1vZGVsLiANCg0KVGhlIHRvcGljIGlzIGFib3V0ICoqb25saW5lIGF1Y3Rpb25zKiogdXNpbmcgYW4gb25saW5lIGdhbWUgY2FsbGVkIGBHdWlsZCBXYXJzIDJgIGFzIGFuIGV4YW1wbGUuIFRoZSBkYXRhIHdhcyBmZXRjaGVkIGZyb20gdGhlIFdlYiBBUEkgcHJvdmlkZWQgYnkgdGhlIGdhbWUgZGV2ZWxvcGVycy4gV2UgdGFsayBhYm91dCBhcm91bmQgYDUuMDAwLjAwMCBkYXRhc2V0c2Agc28gSSBoYWQgdG8gY29uY2VudHJhdGUgb24gdGhlIG1vc3QgdmFsdWFibGUgaXRlbXMgZmlyc3QuDQoNClRoZSBnb2FsIHdhcyB0byBmaW5kIG91dCBpZiB0aGVyZSBhcmUgaXRlbXMgdGhhdCBjYW4gYmUgYm91Z2h0IGxvdyBhbmQgc29sZCBoaWdoIHRvIGVhcm4gKHZpcnR1YWwpIG1vbmV5LiBUaGlzIHdhcyBwcmV0dHkgbXVjaCBwb3NzaWJsZSB3aXRoIGRhdGEgYW5hbHlzaXMgYW5kIHZpc3VhbGl6YXRpb24uIE9mIGNhdXNlIHRob3NlIGl0ZW1zIGFyZSBjaGFuZ2luZyB2ZXJ5IGZhc3QsIGJ1dCBhcyBhIGNvbmNsdXNpb24gSSBjYW4gc2F5IGl0J3Mgbm90IHBvc3NpYmxlIHRvIGJ1eSBhbmQgc2VsbCAidGhlIG9uZSBpdGVtIiwgdGhlIGdhaW5zIGFyZSBwcmV0dHkgbG93IGFuZCBpdCdzIG9ubHkgd29ydGggaXQgd2hlbiBidXlpbmcgaXRlbXMgaW4gYnVsay4NCg0KQXMgdGhlIGRhdGEgcHJldHR5IG11Y2ggY29udGFpbnMgYWxsIGRhdGEgYW5kIGRvZXNuJ3QgeWllbGQgYSByZWFsIHF1ZXN0aW9uLCBmb3IgdGhlIG1vZGVsIHBhcnQsIEkgd2FudGVkIHRvIGZpbmQgb3V0IGFuZCBwcmVkaWN0IGF0IHdoaWNoIHJhdGUgYW4gaXRlbSBjYW4gYmUgc29sZC4gVGhlIHNvYmVyIGNvbnNpZGVyYXRpb24gaXMgdGhhdCBpcyBvbmx5IGRlcGVuZHMgb24gdGhlIGJ1eWluZyBwcmljZSB3aGljaCBzZWxsaW5nIHByaWNlIGNhbiBiZSBhY2hpZXZlZC4gQWxsIG90aGVyIGF0dHJpYnV0ZXMgbGlrZSB0eXBlLCByYXJpdHkgb3IgcmVxdWlyZWQgbGV2ZWwgb2YgdGhlIGl0ZW0gZGlkIG5vdCBoYXZlIGFueSBlZmZlY3Qgb24gdGhlIG91dGNvbWUuDQoNCiMgSW50cm9kdWN0aW9uDQoNCk1hbGNvbG0gRm9yYmVzIG9uY2Ugc2FpZCwgIk1vbmV5IGlzbid0IGV2ZXJ5dGhpbmcgYXMgbG9uZyBhcyB5b3UgaGF2ZSBlbm91Z2ggb2YgaXQiLg0KU3VyZSwgbW9uZXkgZG9lc24ndCBidXkgaGFwcGluZXNzLCBidXQgaXQgY2FuIGJ1eSBhIGxvdCBvZiB0aGluZ3MuIFRoaXMgaXMgbm90IG9ubHkgdHJ1ZSBmb3IgdGhlIHJlYWwgd29ybGQsIGJ1dCBlc3BlY2lhbGx5IGluIG9ubGluZSBnYW1lcywgd2hlcmUgbWljcm8gdHJhbnNhY3Rpb25zIGFuZCBwYXllZCBzZXJ2aWNlcyBoYXZlIGJlY29tZSB0aGUgbmV3IG5vcm1hbC4gDQoNCk1vcmUgYW5kIG1vcmUgZ2FtZXMgZ2l2ZSB5b3UgdGhlIGNob2ljZSB0byBpbnZlc3QgYSBsb3Qgb2YgdGltZSAqb3IqIGEgbG90IG9mIG1vbmV5LiBCb3RoIHdheXMgeW91J2xsIHNvbWVob3cgYWNoaWV2ZSB0aGUgZ29hbCBvZiB0aGUgZ2FtZSBmYXN0ZXIuIEluIHNvbWUgZ2FtZXMgeW91IGNhbiBldmVuIGV4Y2hhbmdlIGRpZ2l0YWwgZ29vZHMgZm9yIHJlYWwgbW9uZXkuDQoNClRvIGdldCB0byB0aGUgcG9pbnQsIGhhdmluZyBkaWdpdGFsIGN1cnJlbmN5IGluIGdhbWVzIGNhbiBzYXZlIHlvdSB0aW1lLCBzdHJlc3MgYW5kIGV2ZW4gcmVhbCBtb25leS4NCg0KVGhlcmUgYXJlIG11bHRpcGxlIHdheXMgdG8gZWFybiBtb25leSBpbiBnYW1lcy4NCk9uZSB3YXkgbWF5IGJlIGZhcm1pbmcsIHdoaWNoIG1lYW5zIGh1bnRpbmcgc3BlY2lhbCBpdGVtcyBpbiBoaWdoIGFtb3VudHMgYW5kIHNlbGxpbmcgdGhlbSB0byBvdGhlciBwbGF5ZXJzLiBIZXJlJ3MgdGhlIHF1ZXN0aW9uLCB3aGljaCBpdGVtcyBhcmUgd29ydGggY29sbGVjdGluZyBhbmQgY2FuIGJlIHNvbGQgZm9yIHdoaWNoIHByaWNlcz8NCg0KQW5vdGhlciB3YXkgaXMgYnV5aW5nIGl0ZW1zIGZyb20gb3RoZXIgcGxheWVycyBhbmQgcmVzZWxsaW5nIHRoZW0gd2l0aCBhIGhpZ2hlciBwcmljZSB0YWcsIGp1c3QgbGlrZSBpbiB0aGUgcmVhbCB3b3JsZC4NCk1hbnkgZ2FtZXMgaGF2ZSBhdWN0aW9uIGhvdXNlcyB3aGVyZSB0aG9zZSB0cmFuc2FjdGlvbnMgY2FuIGJlIG1hZGUuIEZvciB0aGlzIHR5cGUgb2YgaW5jb21lIG9uZSBuZWVkcyB0byBrbm93IHdoYXQgdG8gYnV5IHdoZW4sIHdoaWNoIHByaWNlcyBhcmUgbG93IG9yIGhpZ2gsIGFuZCB3aGVuIHRvIHNlbGwgZm9yIHdoaWNoIGFtb3VudC4NCg0KRm9yIHRoaXMgcHJvamVjdCBteSBleGFtcGxlIG9mIGdhbWUgd2lsbCBiZSBHdWlsZCBXYXJzIDIuIEd1aWxkIFdhcnMgMiB3YXMgcHVibGlzaGVkIGluIDIwMTIgYnkgTkNTb2Z0LiBUaGVyZSBhcmUgbm8gbW9udGhseSBmZWVzIGFuZCB0aGUgYmFzaWMgZ2FtZSBpcyBmcmVlIHRvIHBsYXkgc2luY2UgMjAxNS4gSXQgaXMgYSBtYXNzaXZlbHkgbXVsdGlwbGF5ZXIgb25saW5lIHJvbGUtcGxheWluZyBnYW1lLCBtZWFuaW5nIGEgbG90IG9mIHBlb3BsZSBwbGF5aW5nIGluIHBhcmFsbGVsIGluIGFuIG9ubGluZSB3b3JsZC4gVGhlcmUgYXJlIG92ZXIgMjAuMDAwIGl0ZW1zIHRoYXQgY2FuIGJlIGNvbGxlY3RlZCBhbmQgbW9zdCBvZiB0aGVtIGNhbiBiZSBzb2xkIGFuZCBib3VnaHQgYXQgdGhlIGF1Y3Rpb24gaG91c2UsIGNhbGxlZCB0aGUgdHJhZGluZyBwb3N0Lg0KDQohW1RoZSB0cmFkaW5nIHBvc3RdKGltYWdlcy9UcmFkaW5nX1Bvc3RfaG9tZS5qcGcpDQoNCldoZW4gc2VsbGluZyBpdGVtcywgNSUgb2YgdGhlIGNvc3QgaXMgYSBmZWUgZm9yIHRoZSBhdWN0aW9uIGhvdXNlIGFuZCBnZXRzIGltbWVkaWF0ZWx5IHRha2VuIGZyb20gdGhlIHdhbGxldC4NCkVhY2ggdGltZSBhbiBpdGVtIGlzIHBsYWNlZCBpbiB0aGUgYXVjdGlvbiBob3VzZSwgdGhpcyA1JSBmZWUgbXVzdCBiZSBwYXllZCwgc28gaXQncyBiZXN0IGlmIHRoZSBpdGVtcyBnZXQgc29sZCBvbiB0aGUgZmlyc3QgcnVuLg0KDQpXaGVuIGJ1eWluZyBpdGVtcywgMTAlIG9mIHRoZSBwcmljZSBnb2VzIHRvIHRoZSBiYW5rIGJlZm9yZSB0aGUgcmVzdCBpcyBkZWxpdmVyZWQgdG8gdGhlIHNlbGxlci4NCg0KQmVjYXVzZSBvZiB0aGF0IGl0J3MgaW1wb3J0YW50IHRvIGZpbmQgaXRlbXMgd2hlcmUgdGhlIGJ1eS10by1zZWxsIHByaWNlIHJhdGlvIGlzIHRoZSBoaWdoZXN0Lg0KSW4gYWRkaXRpb24sIGFzIHdlIGJ1eSAqKmFuZCoqIHNlbGwsIHdlIGhhdmUgdG8gcGF5IHRoZSB3aG9sZSAxNSUgdGF4ZXMgd2hpY2ggbG93ZXJzIG91ciByZXZlbnVlLg0KDQpIZXJlIGlzIG9uZSBleGFtcGxlOg0KDQpUaGlzIGlzIGhvdyBpdCB3b3JrczoNCg0KMS4gV2Ugd2FudCB0byBidXkgYW4gaXRlbSBzbyB3ZSBjcmVhdGUgYSBuZXcgb3JkZXIgd2l0aCB0aGUgaGlnaGVzdCBvcmRlciBwcmljZSAoc2F5OiAxIGdvbGQpDQoyLiBBcyBzb29uIGFzIGFub3RoZXIgcGxheWVyIHdhbnRzIHRvIHNlbGwgdGhpcyBpdGVtLCB0aGUgcGxheWVyIHdpbGwgc2VsbCBpdCB0byB1cyAoZm9yIDEgZ29sZCkNCjMuIFdlIHRoZW4gY3JlYXRlIGEgbmV3IGF1Y3Rpb24gZm9yIHRoaXMgaXRlbSB1c2luZyB0aGUgbG93ZXN0IG9mZmVyIHByaWNlIChzYXk6IDIgZ29sZCkuIFdlIGhhdmUgdG8gcGF5IDUlIGZvciBjcmVhdGluZyB0aGlzIGF1Y3Rpb24uDQo0LiBBcyBzb29uIGFzIGFub3RoZXIgcGxheWVyIHdhbnRzIHRvIGJ1eSB0aGlzIGl0ZW0sIHRoZSBwbGF5ZXIgd2lsbCBidXkgaXQgZnJvbSB1cyBmb3IgMiBnb2xkLiBEdXJpbmcgdGhpcyB0cmFuc2FjdGlvbiwgMTAlIGlzIGEgZmVlIGZvciB0aGUgYmFuaywgc28gd2UgZ2V0IDEwJSBsZXNzLg0KDQpPdmVyYWxsLCB3ZSBpbnZlc3RlZCBgMS4wNSBnb2xkYCBhbmQgZWFybmVkIGAxLjggZ29sZGAuIFRoaXMgaXMgYSByZXZlbnVlIG9mIChvbmx5KSBgMC43NSBnb2xkYC4NCg0KIVtDb2lucyBhcmUgdGhlIGJhc2ljIGN1cnJlbmN5XShpbWFnZXMvY29pbi5wbmcpDQoNCkp1c3QgbGlrZSBpbiB0aGUgcmVhbCB3b3JsZCwgd2hlcmUgMTAwIGNlbnRzIGFyZSAxIEV1cm8sIHRoZSBtYWluIGN1cnJlbmN5IGluIEd1aWxkIFdhcnMsIGNvaW5zLCBhcmUgc2VwYXJhdGVkIGludG8gdGhyZWUgdW5pdHMuDQpUaGVyZSBpcyAqKmNvcHBlcioqLCAqKnNpbHZlcioqIGFuZCAqKmdvbGQqKi4gDQoNClRoZSBmb2xsb3dpbmcgdmFsdWVzIGFyZSBhbGwgdGhlIHNhbWUgYW5kIGFyZSBhdXRvbWF0aWNhbGx5IGNhbGN1bGF0ZWQgYnkgdGhlIGdhbWU6DQpgDQoxMC4wMDAgY29wcGVyID09IDEwMCBzaWx2ZXIgPT0gMSBnb2xkLg0KDQpGb3IgdGhpcyBwcm9qZWN0LCBkYXRhIGlzIGdlbmVyYWxseSBkaXNwbGF5ZWQgaW4gZ29sZC4NCg0KIyBXb3JraW5nIHdpdGggdGhlIGRhdGENCg0KIyMgRGF0YSBpbmdlc3Rpb24NCg0KV2UgbmVlZCB0byBnZXQgdGhlIGRhdGEgZmlyc3QuIFdlIHNjcmFwIHRoZSBkYXRhIGZyb20gYGh0dHBzOi8vYXBpLmd1aWxkd2FyczIuY29tL3YyL2NvbW1lcmNlL2AuIEZpcnN0IHdlIGdldCBhbGwgYDI2ODAwYCBpdGVtcyBhbmQsIGluIGJhdGNoZXMgb2YgYDIwMGAgaXRlbXMgcGVyIHJlcXVlc3QsIGdldCBhbGwgYXVjdGlvbnMgZm9yIHRoZXNlIGl0ZW1zLiBJdCdzIHJlYWxseSBpbXBvcnRhbnQgdG8gdXNlIHRoZSBiYXRjaCBlbmRwb2ludCB0byBrZWVwIHRoZSBydW50aW1lIHRvIGEgcmVhc29uYWJsZSBkdXJhdGlvbi4NCg0KYGBge3IgbG9hZF9jc3YsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpkaXIgPC0gZ2V0d2QoKQ0KZGF0ZSA8LSBwYXJhbXMkZGF0YV9kYXRlDQoNCnByaWNlX2xpc3RfYnV5cyA8LSByZWFkLmNzdihwYXN0ZShkaXIsICIvLi4vZGF0YS9yYXcvZ3cyLWFsbC1idXlzLXJhdy0iLCBkYXRlLCAiLmNzdiIsIHNlcCA9ICIiKSkNCnByaWNlX2xpc3Rfc2VsbHMgPC0gcmVhZC5jc3YocGFzdGUoZGlyLCAiLy4uL2RhdGEvcmF3L2d3Mi1hbGwtc2VsbHMtcmF3LSIsIGRhdGUsICIuY3N2Iiwgc2VwID0gIiIpKQ0KaXRlbV9saXN0IDwtIHJlYWQuY3N2KHBhc3RlKGRpciwgIi8uLi9kYXRhL3Jhdy9ndzItYWxsLWl0ZW1zLXJhdy0iLCBkYXRlLCAiLmNzdiIsIHNlcCA9ICIiKSkNCg0KZGZfYnV5cyA8LSBpdGVtX2xpc3QgJT4lIGxlZnRfam9pbihwcmljZV9saXN0X2J1eXMsIGJ5ID0gImlkIikNCmRmX3NlbGxzIDwtIGl0ZW1fbGlzdCAlPiUgbGVmdF9qb2luKHByaWNlX2xpc3Rfc2VsbHMsIGJ5ID0gImlkIikNCg0Kcm0oZGlyKQ0Kcm0oZGF0ZSkNCnJtKGl0ZW1fbGlzdCkNCnJtKHByaWNlX2xpc3Rfc2VsbHMpDQpybShwcmljZV9saXN0X2J1eXMpDQpgYGANCg0KRmlyc3Qgd2UgY2xlYW4gdGhlIGRhdGEgYW5kIGNoYW5nZSBzb21lIGZlYXR1cmUgdHlwZXMuDQpBcyBhbHJlYWR5IHNhaWQsIG1vc3Qgb2YgdGhlIHRpbWUgd2UgdXNlIHRoZSBwcmljZSBpbiBnb2xkLCBzbyB3ZSBhZGQgdGhpcyBmZWF0dXJlLg0KDQpgYGB7ciBkYXRhX2NsZWFuaW5nIH0NCmRmX3NlbGxzIDwtIGRmX3NlbGxzICU+JSANCiAgZHJvcF9uYSh1bml0X3ByaWNlLCBxdWFudGl0eSkgJT4lIA0KICBtdXRhdGUocmFyaXR5ID0gYXMuZmFjdG9yKHJhcml0eSksDQogICAgICAgICB0eXBlID0gYXMuZmFjdG9yKHR5cGUpLA0KICAgICAgICAgaXRlbV90eXBlID0gYXMuZmFjdG9yKGl0ZW1fdHlwZSksDQogICAgICAgICBpdGVtX3dlaWdodF9jbGFzcyA9IGFzLmZhY3RvcihpdGVtX3dlaWdodF9jbGFzcyksDQogICAgICAgICB1bml0X3ByaWNlX2dvbGQgPSB1bml0X3ByaWNlIC8gMTAwMDApICU+JSANCiAgc2VsZWN0KC11bml0X3ByaWNlKQ0KDQpkZl9idXlzIDwtIGRmX2J1eXMgJT4lIA0KICBkcm9wX25hKHVuaXRfcHJpY2UsIHF1YW50aXR5KSAlPiUgDQogIG11dGF0ZShyYXJpdHkgPSBhcy5mYWN0b3IocmFyaXR5KSwNCiAgICAgICAgIHR5cGUgPSBhcy5mYWN0b3IodHlwZSksDQogICAgICAgICBpdGVtX3R5cGUgPSBhcy5mYWN0b3IoaXRlbV90eXBlKSwNCiAgICAgICAgIGl0ZW1fd2VpZ2h0X2NsYXNzID0gYXMuZmFjdG9yKGl0ZW1fd2VpZ2h0X2NsYXNzKSwNCiAgICAgICAgIHVuaXRfcHJpY2VfZ29sZCA9IHVuaXRfcHJpY2UgLyAxMDAwMCkgJT4lIA0KICBzZWxlY3QoLXVuaXRfcHJpY2UpDQpgYGANCg0KDQpBZnRlciBhY3F1aXJpbmcgdGhlIGRhdGEgdGhpcyB3YXkgd2UgaGF2ZSBvbmUgZmlsZSB3aXRoIGFsbCB0aGUgKippdGVtcyoqLCBvbmUgd2l0aCBhbGwgdGhlICoqc2VsbCBvcmRlcnMqKiAoYHIgbnJvdyhkZl9zZWxscylgKSBhbmQgb25lIHdpdGggYWxsIHRoZSAqKmJ1eSBvcmRlcnMqKiAoYXJvdW5kIGByIG5yb3coZGZfYnV5cylgKS4gV2hlbiBqb2luaW5nIHRob3NlIGZpbGVzIHRvZ2V0aGVyLCB0aGUgYW1vdW50IG9mIGRhdGEgd291bGQgYmUgZW5vcm1vdXMuDQoNCiMjIyBGaWx0ZXIgb24gaGlnaGVzdCBidXkgYW5kIGxvd2VzdCBzZWxsIG9yZGVycw0KDQpUaGVyZWZvcmUgc29tZSBmaWx0ZXJpbmcgaXMgcmVxdWlyZWQgZmlyc3QuIFdlIG9ubHkgdGFrZSB0aGUgaGlnaGVzdCBidXkgb3JkZXJzIGFuZCB0aGUgbG93ZXN0IHNlbGwgb3JkZXJzLCBiZWNhdXNlIHRoZXNlIGFyZSB0aGUgZmlyc3Qgb25lcyB0byBiZSBib3VnaHQvc29sZC4NCg0KYGBge3IgZmlsdGVyX2J1eXNfc2VsbHN9DQpkZl9tYXhfYnV5cyA8LSAgZGZfYnV5cyAlPiUgDQogIGdyb3VwX2J5KG5hbWUpICU+JSANCiAgc2xpY2Uod2hpY2gubWF4KHVuaXRfcHJpY2VfZ29sZCkpDQoNCmRmX21pbl9zZWxscyA8LSAgZGZfc2VsbHMgJT4lIA0KICBncm91cF9ieShuYW1lKSAlPiUgDQogIHNsaWNlKHdoaWNoLm1pbih1bml0X3ByaWNlX2dvbGQpKQ0KYGBgDQoNCk5vdyB3ZSBjYW4gam9pbiB0aGUgc2VsbCBhbmQgYnV5IG9yZGVycyBhbmQgY2FsY3VsYXRlIHRoZSBwcm9maXQgcGVyIGl0ZW0gYWNjb3JkaW5nIHRvIHRoZSB0YXggcnVsZXMgZGVzY3JpYmVkIGVhcmxpZXIuDQoNCmBgYHtyIG1lcmdlX2J1eXNfc2VsbHMsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpkZl9hbGwgPC0gZGZfbWF4X2J1eXMgJT4lDQogIG11dGF0ZShxdWFudGl0eV9idXlzID0gcXVhbnRpdHksDQogICAgICAgICB1bml0X3ByaWNlX2dvbGRfYnV5cyA9IHVuaXRfcHJpY2VfZ29sZCkgJT4lIA0KICBzZWxlY3QoLXF1YW50aXR5LCAtdW5pdF9wcmljZV9nb2xkKSAlPiUgDQogIHJpZ2h0X2pvaW4oZGZfbWluX3NlbGxzICU+JSANCiAgICAgICAgICAgICAgbXV0YXRlKHF1YW50aXR5X3NlbGxzID0gcXVhbnRpdHksDQogICAgICAgICAgICAgICAgICAgICB1bml0X3ByaWNlX2dvbGRfc2VsbHMgPSB1bml0X3ByaWNlX2dvbGQpICU+JSANCiAgICAgICAgICAgICAgc2VsZWN0KGlkLCBxdWFudGl0eV9zZWxscywgdW5pdF9wcmljZV9nb2xkX3NlbGxzKSwgYnkgPSAiaWQiKSAlPiUgDQogIG11dGF0ZShuYW1lID0gbmFtZS54KSAlPiUgDQogIHNlbGVjdCgtbmFtZS54LCAtbmFtZS55KQ0KDQpgYGANCg0KYGBge3IgY3JlYXRlX3Byb2ZpdF9mZWF0dXJlIH0NCmRmX2FsbCA8LSBkZl9hbGwgJT4lIA0KICBtdXRhdGUodW5pdF9wcmljZV9nb2xkX2RpZmYgPSB1bml0X3ByaWNlX2dvbGRfc2VsbHMgLSB1bml0X3ByaWNlX2dvbGRfYnV5cywNCiAgICAgICAgIHByb2ZpdCA9IDAuODUgKiB1bml0X3ByaWNlX2dvbGRfc2VsbHMgLSB1bml0X3ByaWNlX2dvbGRfYnV5cywNCiAgICAgICAgIG1vcmVfc2VsbHMgPSBxdWFudGl0eV9zZWxscyAtIHF1YW50aXR5X2J1eXMpDQpgYGANCg0KQXMgd2UgY2FuIHNlZSB0aGVyZSBhcmUgc29tZSB2ZXJ5IGhpZ2ggb3V0bGllcnMgYW5kIGEgbG90IG9mIG91dGxpZXJzIGJldHdlZW4gYXJvdW5kIDEwMCBhbmQgMzAwMCBnb2xkLg0KV2UgbmVlZCB0byB6b29tIGluIG11Y2ggbW9yZSB0byBzZWUgdGhlIGRldGFpbHMuDQoNCmBgYHtyIHBsb3Rfb3V0bGllcnN9DQpkZl9hbGwgJT4lIA0KZ2dwbG90KCkgKyAgDQogIGdlb21fYm94cGxvdChhZXMoeCA9ICdTZWxscycsIHkgPSB1bml0X3ByaWNlX2dvbGRfc2VsbHMpKSArDQogIGdlb21fYm94cGxvdChhZXMoeCA9ICdCdXlzJywgeSA9IHVuaXRfcHJpY2VfZ29sZF9idXlzKSkgKw0KICBnZW9tX2JveHBsb3QoYWVzKHggPSAnUHJvZml0JywgeSA9IHByb2ZpdCkpICsNCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMzAwMCwgbGluZXR5cGU9ImRhc2hlZCIsIGNvbG9yID0gInJlZCIpICsNCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMTAwLCBsaW5ldHlwZT0iZGFzaGVkIiwgY29sb3IgPSAiYmx1ZSIpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IGNvbW1hKSArDQogIGxhYnModGl0bGUgPSAiT3V0bGllcnMgb24gYnV5cyBhbmQgc2VsbHMiLCBzdWJ0aXRsZSA9ICJCYXNlbGluZSBiZXR3ZWVuIDEwMCBhbmQgMzAwMCBnb2xkIiwNCiAgICB4ID0gIiIsIHkgPSAiUHJpY2UgaW4gZ29sZCIsIGNhcHRpb24gPSBwYXN0ZSgiRGF0YSBmcm9tIiwgcGFyYW1zJGRhdGFfZGF0ZSkpDQpgYGANCg0KYGBge3IgcGxvdF9vdXRsaWVyc196b29tX3NlbGxzIH0NCmRmX2FsbCAlPiUgDQogIHN1YnNldChwcm9maXQgPCAyLjUgJiB1bml0X3ByaWNlX2dvbGRfc2VsbHMgPCAyLjUpICU+JSANCmdncGxvdCgpICsgIA0KICBnZW9tX2JveHBsb3QoYWVzKHggPSAnU2VsbHMnLCB5ID0gdW5pdF9wcmljZV9nb2xkX3NlbGxzKSkgKw0KICBnZW9tX3RleHQoYWVzKHggPSAnU2VsbHMnLCB5ID0gbWVkaWFuKHVuaXRfcHJpY2VfZ29sZF9zZWxscyksIGxhYmVsID0gbWVkaWFuKHVuaXRfcHJpY2VfZ29sZF9zZWxscykpLCBzaXplID0gMywgdmp1c3QgPSAtMSkgKw0KICBnZW9tX2JveHBsb3QoYWVzKHggPSAnUHJvZml0JywgeSA9IHByb2ZpdCkpICsNCiAgZ2VvbV90ZXh0KGFlcyh4ID0gJ1Byb2ZpdCcsIHkgPSBtZWRpYW4ocHJvZml0KSwgbGFiZWwgPSBtZWRpYW4ocHJvZml0KSksIHNpemUgPSAzLCB2anVzdCA9IC0wLjUpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IGNvbW1hKSArDQogIGxhYnModGl0bGUgPSAiT3V0bGllcnMgb24gcHJvZml0IGFuZCBzZWxscyIsIHN1YnRpdGxlID0gIkxpbWl0IGF0IDIuNSBnb2xkIHByb2ZpdCBhbmQgc2VsbCBwcmljZSIsDQogICAgeCA9ICIiLCB5ID0gIlByaWNlIGluIGdvbGQiLCBjYXB0aW9uID0gcGFzdGUoIkRhdGEgZnJvbSIsIHBhcmFtcyRkYXRhX2RhdGUpKQ0KDQptZWRpYW5fcHJvZml0X3NpbHZlciA8LSByb3VuZCgxMDAgKiBkZl9hbGwgJT4lIA0KICBkcm9wX25hKHByb2ZpdCkgJT4lIA0KICBzdW1tYXJpc2UobWVkaWFuKHByb2ZpdCkpICU+JSANCiAgZmlyc3QoKSwgMikNCg0KbWVkaWFuX3Byb2ZpdF9zaWx2ZXJfZmlsdGVyIDwtIHJvdW5kKDEwMCAqIGRmX2FsbCAlPiUgDQogIHN1YnNldChwcm9maXQgPCAyLjUgJiB1bml0X3ByaWNlX2dvbGRfc2VsbHMgPCAyLjUpICU+JSANCiAgZHJvcF9uYShwcm9maXQpICU+JSANCiAgc3VtbWFyaXNlKG1lZGlhbihwcm9maXQpKSAlPiUgDQogIGZpcnN0KCksIDIpDQpgYGANCg0KDQo+IFRoZSBtZWRpYW4gcHJvZml0IGlzIG5vdCBpbiB0aGUgZ29sZCByYW5nZSwgYnV0IGF0IGByIG1lZGlhbl9wcm9maXRfc2lsdmVyYCBzaWx2ZXIuIFdoZW4gZmlsdGVyaW5nIHdpdGggcmVhc29uYWJsZSBib3VuZHMsIHRoZSBwcm9maXQgZXZlbiBmYWxscyBkb3duIHRvIGByIG1lZGlhbl9wcm9maXRfc2lsdmVyX2ZpbHRlcmAgc2lsdmVyLg0KDQojIyMgRmlsdGVyIG9uIHJlYWxpc3RpYyBwcm9maXRzDQoNCldlIGNhbiBub3cgc3RyaXAgZG93biB0aGUgZGF0YSBldmVuIGZ1cnRoZXIuDQoNCkxldCdzIHRha2Ugb25seSB0aGUgaXRlbXMgd2l0aCBhIHJlYWxpc3RpYyBwcm9maXQgYW5kIHN0cmlwIGF3YXkgdGhlIGl0ZW1zIHdoZXJlIGl0IHdvdWxkIGJlIGJldHRlciB0byBzZWxsIHRoZW0gdG8gdGhlIG5vbi1wbGF5ZXIgdmVuZG9yIGluc3RlYWQgb2YgcGxhY2luZyB0aGVtIGluIHRoZSBhdWN0aW9uIGhvdXNlLg0KDQpgYGB7ciBmaWx0ZXJfYnlfcHJvZml0IH0NCmRmX2FsbCA8LSBkZl9hbGwgJT4lIA0KICBzdWJzZXQocHJvZml0ID4gMC4wNCAmIHByb2ZpdCA8IDAuNCkNCg0KZGZfYWxsIDwtIGRmX2FsbCAlPiUgDQogIHN1YnNldChwcm9maXQgKiAxMDAgPiB2ZW5kb3JfdmFsdWUpICU+JSANCiAgYXJyYW5nZShkZXNjKHByb2ZpdCkpDQpgYGANCg0KTm93IG9ubHkgYHIgbnJvdyhkZl9hbGwpYCBpdGVtcyBsZWZ0Lg0KTGV0J3MgaGF2ZSBhIGNsb3NlciBsb29rIGF0IHRob3NlIGl0ZW1zLg0KDQpgYGB7ciBkZl9hbGxfb3ZlcnZpZXcsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQoNCmRmX2FsbCAlPiUNCiAgZ3JvdXBfYnkodHlwZSwgcmFyaXR5KSAlPiUgDQogIHN1bW1hcmlzZShuYW1lID0gdW5pcXVlKG5hbWUpLA0KICAgICAgICAgICAgcHJvZml0ID0gcHJvZml0ICogMTAwLA0KICAgICAgICAgICAgaWNvbiA9IG1pbih3ZWJfaW1hZ2UoaWNvbiwgaGVpZ2h0ID0gNTApKQ0KICAgICAgICAgICAgKSAlPiUgDQogIGRyb3BfbmEoKSAlPiUgDQogIGFycmFuZ2UoZGVzYyhwcm9maXQpKSAlPiUNCiAgZ3Qocm93bmFtZV9jb2wgPSAibmFtZSIpICU+JQ0KICB0YWJfaGVhZGVyKHRpdGxlID0gIlJlYWxpc3RpYyB0b3AgcHJvZml0IGl0ZW1zIiwgc3VidGl0bGUgPSAiIikgJT4lDQogIGZtdF9udW1iZXIoDQogICAgY29sdW1ucyA9IHByb2ZpdCwNCiAgICBzdWZmaXhpbmcgPSAiUyINCiAgKSAlPiUgDQogIGZtdF9tYXJrZG93bigNCiAgICBjb2x1bW5zID0gaWNvbg0KICApICU+JSANCiAgc3VtbWFyeV9yb3dzKA0KICAgIGdyb3VwcyA9IFRSVUUsDQogICAgY29sdW1ucyA9IHByb2ZpdCwNCiAgICBmbnMgPSBsaXN0KGF2ZXJhZ2UgPSAibWVhbiIpLA0KICAgIGZvcm1hdHRlciA9IGZtdF9udW1iZXINCiAgKSAlPiUNCiAgdGFiX2Zvb3Rub3RlKA0KICAgIGZvb3Rub3RlID0gIlByaWNlcyBpbiBzaWx2ZXIiLA0KICAgIGxvY2F0aW9ucyA9IGNlbGxzX2NvbHVtbl9sYWJlbHMoY29sdW1ucyA9IHByb2ZpdCkNCiAgKSAlPiUNCiAgdGFiX3NvdXJjZV9ub3RlKA0KICAgICJCYXNlZCBvbiBkYXRhIGZyb20gYXBpLmd1aWxkd2FyczIuY29tIg0KICApICU+JSANCiAgdGFiX29wdGlvbnMoDQogICAgc3VtbWFyeV9yb3cuYmFja2dyb3VuZC5jb2xvciA9ICIjQUNFQUNFIiwNCiAgICByb3dfZ3JvdXAuYmFja2dyb3VuZC5jb2xvciA9ICIjRkZFRkRCIiwNCiAgICB0YWJsZS5sYXlvdXQgPSAiYXV0byIsDQogICAgY29udGFpbmVyLm92ZXJmbG93LnggPSBUUlVFLA0KICAgIGNvbnRhaW5lci5oZWlnaHQgPSBweCgzNTApDQogICkNCmBgYA0KDQojIyBEYXRhIHNwbGl0dGluZw0KDQpUbyBtYWtlIHRoZSBkYXRhIHNwbGl0IHJlcHJvZHVjaWJsZSwgd2Ugc2V0IGEgc2VlZC4NCldlIHdhbnQgdG8gcHJlZGljdCB0aGUgZ29sZCBwcmljZSBiYXNlZCBvbiBhdHRyaWJ1dGVzIG9mIHRoZSBpdGVtcy4NCg0KYGBge3IgY3JlYXRlX2RhdGFfc3BsaXR9DQpzZXQuc2VlZCg0MikNCg0KIyBQdXQgMy80IG9mIHRoZSBkYXRhIGludG8gdGhlIHRyYWluaW5nIHNldCANCmRhdGFfc3BsaXQgPC0gaW5pdGlhbF9zcGxpdChkZl9hbGwsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJvcCA9IDMvNCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzdHJhdGEgPSBwcm9maXQsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWtzID0gNCkNCg0KIyBDcmVhdGUgZGF0YWZyYW1lcyBmb3IgdGhlIHR3byBzZXRzOg0KdHJhaW5fZGF0YSA8LSB0cmFpbmluZyhkYXRhX3NwbGl0KSANCnRlc3RfZGF0YSA8LSB0ZXN0aW5nKGRhdGFfc3BsaXQpDQpgYGANCg0KV2UgYWxzbyBjcmVhdGUgYSB2YWxpZGF0aW9uIHNldCB1c2luZyB0aGUgQ1ZfZm9sZCBtZXRob2QgdGhhdCBpcyB1c2VkIGR1cmluZyBtb2RlbGluZy4NCg0KIyMjID5UT0RPOiB1c2UgY3NfZm9sZHMgc29tZXdoZXJlID8/DQoNCmBgYHtyIGNyZWF0ZV9jdl9mb2xkc30NCmN2X2ZvbGRzIDwtIA0KICB2Zm9sZF9jdih0cmFpbl9kYXRhLA0KICAgICAgICAgICB2PTUsDQogICAgICAgICAgIHN0cmF0YSA9IHByb2ZpdCwNCiAgICAgICAgICAgYnJlYWtzID0gNCkNCg0KZGZfdHJhaW4gPC0gdHJhaW5fZGF0YSANCmBgYA0KDQpGcm9tIG5vdyBvbiB3ZSB3aWxsIHdvcmsgd2l0aCB0aGUgdHJhaW4gZGF0YSAoYGRmX3RyYWluYCkuDQoNCiMjIEFuYWx5emUgZGF0YQ0KDQojIyMgRmluZCBjb3JyZWxhdGlvbnMNCg0KYGBge3Igc3BlYXJtYW5fYW5hbHlzaXN9DQpkZl90cmFpbiAlPiUgDQogIHNlbGVjdCh3aGVyZShpcy5udW1lcmljKSwgLW1vcmVfc2VsbHMsIC1wcm9maXQsIC11bml0X3ByaWNlX2dvbGRfZGlmZikgJT4lICMgb25seSBzZWxlY3QgbnVtZXJpY2FsIGRhdGENCiAgdmlzX2Nvcihjb3JfbWV0aG9kID0gInNwZWFybWFuIiwgbmFfYWN0aW9uID0gInBhaXJ3aXNlLmNvbXBsZXRlLm9icyIpDQpgYGANCg0KV2Ugc2VlIHRoYXQgb3VyIGRhdGEgaXMgcHJldHR5IHVuY29ycmVsYXRlZCB3aGljaCBtYWtlcyBpdCBoYXJkIHRvIGZpbmQgYSBnb29kIGNsYXNzaWZpY2F0aW9uIG1vZGVsLiBXaGF0IHdlIHNlZSBpcyB0aGF0IGJ1eSBhbmQgc2VsbCBwcmljZSBhcmUgc29tZXdoYXQgY29ycmVsYXRlZCBhcyB3ZWxsIGFzIHRoZSBsZXZlbCBvZiB0aGUgaXRlbSB0byB0aGUgdmVuZG9yIHZhbHVlLg0KDQpUaGUgbGF0dGVyIG1ha2VzIHNlbnNlLCBhcyB0aGVyZSBtdXN0IGJlIHNvbWUga2luZCBvZiBhbGdvcml0aG0gdGhhdCBzZXRzIHRoZSBzZWxsIHByaWNlLCBwcm9iYWJseSBhbHNvIGJhc2VkIG9uIHRoZSBpdGVtJ3MgbGV2ZWwuDQoNCj4gRm9sbG93aW5nIGlkZWE6IEEgbW9kZWwgdGhhdCBwcmVkaWN0cyB0aGUgc2VsbCBwcmljZSBiYXNlZCBvbiB0aGUgYnV5IHByaWNlLiBUaGVuIHdlIHNlYXJjaCBmb3Igb3V0bGllcnMgd2hlcmUgdGhlIHNlbGwgcHJpY2Ugd2FzIHByZWRpY3RlZCB0b28gaGlnaC4gUHJvYmFibHkgdGhvc2UgaXRlbXMgYXJlIHVuZGVycmF0ZWQgc29tZWhvdz8NCg0KIyMjIFByaWNlIGRpc3RyaWJ1dGlvbg0KDQpMZXQncyBoYXZlIGEgbG9vayBhdCB0aGUgZGlzdHJpYnV0aW9uIG9mIHByb2ZpdCBpbiBnZW5lcmFsIGZvciB0aGVzZSBpdGVtcy4gV2UgY2FuIHNlZSB0aGF0IHRoZSB0eXBlcyBhcmUgZGlzdHJpYnV0ZWQgcHJldHR5IGV2ZW5seSB3aXRoaW4gb3VyIHNtYWxsIHByb2ZpdCByYW5nZS4NCg0KYGBge3IgcHJvZml0X2Rpc3RyaWJ1dGlvbn0NCmRmX3RyYWluX2Rpc3RyaWJ1dGlvbiA8LSBkZl90cmFpbiAlPiUgDQogIGdyb3VwX2J5KG5hbWUpICU+JSANCiAgc3VtbWFyaXNlKG1lYW5fcHJvZml0ID0gbWVhbihwcm9maXQpLA0KICAgICAgICAgICAgdHlwZSA9IHVuaXF1ZSh0eXBlKSwNCiAgICAgICAgICAgIHJhcml0eSA9IHVuaXF1ZShyYXJpdHkpKSAlPiUgDQogIGFycmFuZ2UoZGVzYyhtZWFuX3Byb2ZpdCkpDQogIA0KZGZfdHJhaW5fZGlzdHJpYnV0aW9uICU+JSANCiAgZ2dwbG90KCkgKw0KICBnZW9tX2hpc3RvZ3JhbShhZXMoeCA9ICBtZWFuX3Byb2ZpdCwgZmlsbCA9IHR5cGUpLCBzdGF0PSJjb3VudCIpICsNCiAgc2NhbGVfeF9iaW5uZWQobGltaXRzID0gYygwLCAwLjQpKSArDQogIGxhYnMoeCA9ICJNZWFuIHByb2ZpdCIsIHkgPSAiQ291bnQiLA0KICAgICAgIHRpdGxlID0gIkl0ZW0gcHJvZml0IGRpc3RyaWJ1dGlvbiIsIHN1YnRpdGxlID0gIkl0ZW1zIGJ5IHByb2ZpdCwgaW4gZ29sZCIsIA0KICAgICAgIGNhcHRpb24gPSBwYXN0ZSgiRGF0YSBmcm9tIiwgcGFyYW1zJGRhdGFfZGF0ZSkpDQpgYGANCg0KDQojIyMgQ2x1c3RlciBBbmFseXNpcw0KDQpUbyBnZXQgYSBiZXR0ZXIgb3ZlcnZpZXcgaG93IHRoZSBpdGVtIGJ1eSBjb3N0cyBhbmQgcHJvZml0cyByZWxhdGUgdG8gZWFjaCBvdGhlciwgd2UnbGwgZG8gYSBjbHVzdGVyIGFuYWx5c2lzLg0KDQoodGFrZW4gZnJvbSBodHRwczovL3d3dy5raXJlbnouY29tL3Bvc3QvMjAyMC0wNS0yMS1yLWhpZXJhcmNoaXNjaGUtY2x1c3RlcmFuYWx5c2UvKQ0KDQpgYGB7ciBzY2FsZV9wcm9maXRfY29zdHN9DQpkZl9jbCA8LSBkZl90cmFpbiAlPiUgDQogIHNlbGVjdChjKCJpZCIsICJwcm9maXQiLCAidW5pdF9wcmljZV9nb2xkX2J1eXMiKSkNCg0KZGZfY2wkcHJvZml0IDwtIHNjYWxlKGRmX2NsJHByb2ZpdCwgY2VudGVyID0gVFJVRSwgc2NhbGUgPSBUUlVFKQ0KZGZfY2wkdW5pdF9wcmljZV9nb2xkX2J1eXMgPC0gc2NhbGUoZGZfY2wkdW5pdF9wcmljZV9nb2xkX2J1eXMsIGNlbnRlciA9IFRSVUUsIHNjYWxlID0gVFJVRSkNCmBgYA0KDQpgYGB7ciBjYWxjdWxhdGVfY2x1c3Rlcn0NCmQgPC0gDQogIGRmX2NsICU+JSANCiAgc2VsZWN0KC1pZCkgJT4lIA0KICBkaXN0KG1ldGhvZCA9ICJldWNsaWRlYW4iKQ0KDQpoYyA8LSBoY2x1c3QoZCwgbWV0aG9kID0gIndhcmQuRDIiKSANCnBsb3QoaGMpIA0KYGBgDQpUaGUgZGVuZHJvZ3JhbSBkaXNwbGF5cyB0aGUgbnVtYmVyIG9mIGNsdXN0ZXJzLiBUaGUgaGlnaGVyIHRoZSBudW1iZXIsIHRoZSBsZXNzIHNpbWlsYXIgYXJlIHRoZSBjbHVzdGVycyB0byBlYWNoIG90aGVyLg0KQmVjYXVzZSB0aGVyZSBpcyBhIGJpZyBnYXAgYmV0d2VlbiB+IDUgYW5kIH4gMTAgd2hpY2ggd291bGQgcmVzdWx0IGluIG9ubHkgdHdvIGNsdXN0ZXJzLCBmb3VyIGNsdXN0ZXJzIHNlZW0gZmluZS4NCg0KV2UgY2FuIGFsc28gdHJ5IHNpeCBjbHVzdGVycywgYXMgdHdvIG9mIHRoZSBjbHVzdGVycyBzZWVtIHRvIGJlIHByZXR0eSBzbWFsbC4NCg0KDQpgYGB7ciBwbG90X2l0ZW1zX2NsdXN0ZXJlZF80fQ0KDQpoYyRsYWJlbHMgPC0gZGZfY2wkaWQNCg0KZ3JwIDwtIGN1dHJlZShoYywgayA9IDQpIA0KZGZfY2wkY2x1c3RlciA8LSBncnANCg0KZGZfY2wgJT4lIA0KICBnZ3Bsb3QoYWVzKHVuaXRfcHJpY2VfZ29sZF9idXlzLCANCiAgICAgICAgICAgICBwcm9maXQsIA0KICAgICAgICAgICAgIGNvbG9yID0gZmFjdG9yKGNsdXN0ZXIpKSkgKw0KICBnZW9tX3BvaW50KCkgKw0KICAjIGdlb21fdGV4dChhZXMobGFiZWwgPSBpZCksIHNpemUgPSAzLCBjaGVja19vdmVybGFwID0gRkFMU0UsIHZqdXN0ID0gMCwgbnVkZ2VfeSA9IDAuMSkgKw0KICB4bGFiKCJCdXkgQ29zdHMiKSArDQogIHlsYWIoIlByb2ZpdCIpICsNCiAgdGhlbWUobGVnZW5kLnRpdGxlPWVsZW1lbnRfYmxhbmsoKSkNCg0Kcm0oZCkNCnJtKGhjKQ0Kcm0oZ3JwKQ0KYGBgDQpCZWNhdXNlIHdlIHNjYWxlZCB0aGUgbnVtYmVycyB3ZSBjYW4ndCByZWFsbHkgc2F5IHNvbWV0aGluZyBhYm91dCB0aGUgcmVhbCB2YWx1ZSBvZiB0aGUgaXRlbXMsIGJ1dCBtb3N0IG9mIHRoZW0gYXJlIGluIHRoZSBsb3dlciBjb3N0IC8gbG93ZXIgcHJvZml0IHJhbmdlLg0KDQpGb3VyIGNsdXN0ZXJzIHdlcmUgY3JlYXRlZCBvdXQgb2YgdGhlIGRhdGE6DQoNCi0gVGhlICIoMSkgcmVkIiBjbHVzdGVyIGFyZSBsb3ctY29zdCBpdGVtcyB3aXRoIGxvdyBwcm9maXQgKGRvbid0IGJ1eXMpDQotIFRoZSAiKDIpIGdyZWVuIiBjbHVzdGVyIGFyZSBoaWdoLWNvc3QgaXRlbXMgd2l0aCBhIGhpZ2ggdmFyaWFuY2UgaW4gcHJvZml0IChtYXliZSBtb3JlIG91dGxpZXJzKQ0KLSBUaGUgIigzKSB0ZWFsIiBjbHVzdGVyIGFyZSBtaWQtY29zdCBpdGVtcyB3aXRoIGRpZmZlcmVudCBwcm9maXQNCi0gVGhlICIoNCkgcHVycGxlIiBjbHVzdGVyIGFyZSBsb3ctY29zdCBpdGVtcyB3aXRoIGhpZ2ggcHJvZml0IChzaG91bGQgYnV5cykNCg0KPiBUaGUgcHJvZml0IGlzIG5vdCBhIHF1ZXN0aW9uIG9mIGJ1eSBjb3N0cywgdGhlcmUgYXJlIGl0ZW1zIHdpdGggaGlnaCBwcm9maXQgZm9yIGxvdyBhbmQgZm9yIGhpZ2ggY29zdHMuIA0KDQo+IFRoZXJlIGFyZSBzb21lIGl0ZW1zIHRoYXQgYXJlIGNoZWFwIGJ1dCB3aWxsIGJyaW5nIGdvb2QgcHJvZml0Lg0KDQojIyMgRmluZCBwcm9maXRhYmxlIGl0ZW1zDQoNClN0YXJ0aW5nIHdpdGggYSBsb3cgYnVkZ2V0LCB3ZSBjYW4ndCBidXkgYSBsb3Qgb2YgaGlnaC1wcmljZWQgaXRlbXMuIFRoZXJlZm9yZSBpdCdzIGdvb2QgdG8ga25vdyB3aGljaCBpdGVtcyBnZXQgdGhlIG1vc3QgcHJvZml0IGNvbXBhcmVkIHRvIHRoZWlyIGNvc3RzLg0KV2UgYWx3YXlzIHN1cHBvc2UgKnRoZSB3b3JzdCogcHJvZml0Lg0KDQpgYGB7ciBnZXRfaGlnaF9yb2kgfQ0KZGZfdHJhaW5faGlnaF9yb2kgPC0gIGRmX3RyYWluICU+JSANCiAgZ3JvdXBfYnkobmFtZSkgJT4lIA0KICBzdW1tYXJpc2UoDQogICAgaWQgPSB1bmlxdWUoaWQpLA0KICAgIHByb2ZpdEJ5Q29zdCA9IG1pbihwcm9maXQpIC8gbWF4KHVuaXRfcHJpY2VfZ29sZF9idXlzKSwNCiAgICBwcm9maXQgPSBtaW4ocHJvZml0KSwNCiAgICBjb3N0ID0gbWF4KHVuaXRfcHJpY2VfZ29sZF9idXlzKSwNCiAgICBzZWxsID0gbWluKHVuaXRfcHJpY2VfZ29sZF9zZWxscyksDQogICAgcXVhbnRpdHkgPSBtaW4ocXVhbnRpdHlfYnV5cykNCiAgKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIGFycmFuZ2UoZGVzYyhwcm9maXRCeUNvc3QpKQ0KYGBgDQoNCmBgYHtyIGRpc3BsYXlfaGlnaF9wcm9maXRfcmVncmVzc2lvbn0NCmRmX3RyYWluX2hpZ2hfcm9pICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gY29zdCwgIHkgPSBwcm9maXQpKSArDQogIGdlb21fcG9pbnQoKSArDQogIGdlb21fc21vb3RoKG1ldGhvZD0nbG0nLCBmb3JtdWxhPSB5fngpICsNCiAgbGFicyh4ID0gIkNvc3QiLCB5ID0gIlByb2ZpdCIsDQogICAgICAgdGl0bGUgPSAiQ29zdCBhbmQgUHJvZml0IG9mIEl0ZW1zIiwgc3VidGl0bGUgPSAiUHJpY2VzIGluIGdvbGQiLA0KICAgICAgIGNhcHRpb24gPSBwYXN0ZSgiRGF0YSBmcm9tIiwgcGFyYW1zJGRhdGFfZGF0ZSkpDQpgYGANCg0KVGhpcyBpcyBiYXNpY2FsbHkgdGhlIHNhbWUgcGljdHVyZSB3ZSBzYXcgZWFybGllciBvbiBjbHVzdGVyIGFuYWx5c2lzLCBidXQgd2l0aCB0aGUgcmVhbCBwcm9maXQgdmFsdWVzLg0KDQpMZXQncyBoYXZlIGEgY2xvc2VyIGxvb2sgYXQgdGhvc2UgaXRlbXMuIEJlY2F1c2UgdGhlIHByaWNlcyBhcmUgaW4gdGhlIGxvd2VyIHNpbHZlciByYW5nZSBhZ2FpbiwgdGhleSBhcmUgZGlzcGxheWVkIGFzIHNpbHZlciBoZXJlOg0KDQpgYGB7ciBwbG90X2hpZ2hfcm9pX3RhYmxlIH0NCmRmX3RyYWluX2hpZ2hfcm9pICU+JSANCiAgbXV0YXRlKA0KICAgIHByb2ZpdEJ5Q29zdCA9IHJvdW5kKHByb2ZpdEJ5Q29zdCwgZGlnaXRzID0gMiksDQogICAgcHJvZml0ID0gcm91bmQocHJvZml0ICogMTAwLCBkaWdpdHMgPSAyKSwNCiAgICBjb3N0ID0gcm91bmQoY29zdCAqIDEwMCwgZGlnaXRzID0gMiksDQogICAgc2VsbCA9IHJvdW5kKHNlbGwgKiAxMDAsIGRpZ2l0cyA9IDIpDQogICAgKSAlPiUgDQogIGZpbHRlcihwcm9maXQgPiAwKSAlPiUgDQogIHNlbGVjdChuYW1lLCBwcm9maXRCeUNvc3QsIHByb2ZpdCwgY29zdCwgc2VsbCwgcXVhbnRpdHkpICU+JSANCiAgZGF0YXRhYmxlKGV4dGVuc2lvbnMgPSBjKCdDb2xSZW9yZGVyJywgJ0J1dHRvbnMnLCAnUmVzcG9uc2l2ZScsICdTY3JvbGxlcicsICdTZWFyY2hQYW5lcycsICdTZWxlY3QnKSwNCiAgICAgICAgICAgIG9wdGlvbnMgPSBsaXN0KGNvbFJlb3JkZXIgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZG9tID0gJ0JmcnRpcCcsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgYnV0dG9ucyA9IGxpc3QoJ3NlYXJjaFBhbmVzJywgJ2NvcHknLCBsaXN0KA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGV4dGVuZCA9ICdjb2xsZWN0aW9uJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBidXR0b25zID0gYygnY3N2JywgJ2V4Y2VsJywgJ3BkZicpLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ0Rvd25sb2FkJykpKSwNCiAgICAgICAgICAgIGVzY2FwZSA9IEZBTFNFLA0KICAgICAgICAgICAgY29sbmFtZXM9YygiTmFtZSIsICJQcm9maXQgYnkgY29zdCIsICJQcm9maXQiLCAiQnV5IGNvc3RzIiwgIlNlbGwgcmV2ZW51ZSIsICJRdWFudGl0eSIpDQogICAgICAgICAgICApDQpgYGANCkdyZWF0LCB3ZSBmb3VuZCBpdGVtcyB3aXRoIHZlcnkgaGlnaCBwcm9maXQuIExldCdzIGJ1eSBzb21lIG9mIHRoZW0gdGhhdCBhbHNvIGhhdmUgYSBoaWdoIHF1YW50aXR5Lg0KDQojIyBNb2RlbA0KDQojIyMgRmVhdHVyZSBTZWxlY3Rpb24NCg0KQXMgd2Ugc2F3IHdoaWxlIGFuYWx5emluZyB0aGUgZGF0YSwgdGhlIGZlYXR1cmVzIGFyZSB1bmNvcnJlbGF0ZWQgZXhjZXB0IGJ1eSBhbmQgc2VsbCBwcmljZXMuDQoNCkFmdGVyIHRlc3RpbmcsIHRoZSBwcmVkaWN0aW9uIGlzIGV2ZW4gYmV0dGVyIHdoZW4gd2UgdGFrZSBvbmx5IHRoZSBgdW5pdF9wcmljZV9nb2xkX2J1eXNgIGZlYXR1cmUgaW50byBhY2NvdW50IGluIG9wcG9zaXRlIHRvIHRha2luZyBtb3JlIGZlYXR1cmVzIGludG8gYWNjb3VudCBsaWtlIGB0eXBlYCwgYHJhcml0eWAgb3IgYGxldmVsYC4gVG8gbWFrZSB0aGlzIGNsZWFyLCB3ZSBmaXJzdCB0YWtlIGFsbCBmZWF0dXJlcyBhbmQgbG9vayBmb3IgY29ycmVsYXRpb25zLg0KDQpgYGB7ciBjcmVhdGVfdHJhaW5fbW9kZWwgfQ0KZGZfdHJhaW4gPC0gdHJhaW5fZGF0YSAlPiUgDQogIHNlbGVjdChpZCwgbmFtZSwgdW5pdF9wcmljZV9nb2xkX3NlbGxzLCB1bml0X3ByaWNlX2dvbGRfYnV5cywgdHlwZSwgcmFyaXR5LCBsZXZlbCkgJT4lIA0KICBkcm9wX25hKCkNCmBgYA0KDQpgYGB7ciBjcmVhdGVfcmVjaXBlIH0NCnNlbGxzX3JlYyA8LSANCiAgcmVjaXBlKHVuaXRfcHJpY2VfZ29sZF9zZWxscyB+IC4sIGRhdGEgPSBkZl90cmFpbikgJT4lIA0KICB1cGRhdGVfcm9sZShpZCwgbmFtZSwgbmV3X3JvbGUgPSAiSUQiKSAlPiUgDQogIHN0ZXBfZHVtbXkoYWxsX25vbWluYWxfcHJlZGljdG9ycygpKSU+JSANCiAgc3RlcF96dihhbGxfcHJlZGljdG9ycygpKSAlPiUgICMgcmVtb3ZlIHplcm8gdmVjdG9ycw0KICBzdGVwX2NlbnRlcihhbGxfcHJlZGljdG9ycygpKSAlPiUNCiAgc3RlcF9zY2FsZShhbGxfcHJlZGljdG9ycygpKQ0KDQpzdW1tYXJ5KHNlbGxzX3JlYykNCmBgYA0KDQpXZSBub3cgaGF2ZSBmb3VyIHByZWRpY3RvcnMgYW5kIG9uZSBvdXRjb21lLiBUaGUgb3RoZXIgZmllbGRzIGFyZSBmb3IgcmVmZXJlbmNlIGlmIHdlIHdhbnQgdG8gZGlnIGRlZXBlciBpbnRvIHRoZSByZXN1bHRzIG9mIHRoZSBtb2RlbC4NCg0KVGhlIHJlY2lwZSBjcmVhdGVzIGR1bW15IHZhcmlhYmxlcyBmb3IgYWxsIG5vbWluYWwgcHJlZGljdG9ycywgdGhpcyBpcyB1c2VmdWwgZm9yIGB0eXBlYCBhbmQgYHJhcml0eWAuDQoNCmB6dmAgd291bGQgcmVtb3ZlcyBhbGwgbi9hIHZlY3RvcnMgd2hpY2ggd291bGQgYmUgaGVscGZ1bCBmb3IgYGl0ZW1fdHlwZWAgYW5kIGBpdGVtX3dlaWdodF9jbGFzc2AgYXMgdGhlc2UgdmFsdWVzIGFyZSBub3QgcHJlc2VudCBmb3IgYWxsIHJvd3MsIGJ1dCB3ZSBkb24ndCB1c2UgdGhlc2UgZmVhdHVyZXMuDQoNCkZpbmFsbHkgdGhlIGB1bml0X3ByaWNlX2dvbGRfYnV5c2Agd2lsbCBiZSBjZW50ZXJlZCBhbmQgc2NhbGVkIHRvIGJldHRlciB3b3JrIHdpdGggdGhlIGFsZ29yaXRobS4NCg0KIyMjIFRyYWluaW5nDQoNCldlIHVzZSB0aGUgYHJhbmRvbSBmb3Jlc3RgIHJlZ3Jlc3Npb24gbW9kZWwgdG8gdHJhaW4gd2l0aCB0aGUgYHJhbmdlcmAgZW5naW5lLCBhbmQgdGhlIGBsaW5lYXIgcmVncmVzc2lvbmAgbW9kZWwgdXNpbmcgdGhlIGBnbG1uZXRgIGVuZ2luZS4NCkluIG9wcG9zaXRlIHRvIHRoZSBgZ2xtYCBlbmdpbmUsIGBnbG1uZXRgIG9ubHkgc3VwcG9ydHMgaW5wdXRzIHdpdGggbW9yZSB0aGFuIG9uZSBwcmVkaWN0b3IuDQoNCmBgYHtyIG1sX3dvcmtmbG93IH0NCiMgcGFyc25pcCBtb2RlbA0Kc2V0LnNlZWQoNDIpDQoNCnJmX21vZCA8LSANCiAgcmFuZF9mb3Jlc3QoKSAlPiUgDQogIHNldF9lbmdpbmUoInJhbmdlciIpICU+JSANCiAgc2V0X21vZGUoInJlZ3Jlc3Npb24iKQ0KDQpsYXNzb19tb2QgPC0gDQogIGxpbmVhcl9yZWcocGVuYWx0eSA9IDAuMSwgbWl4dHVyZSA9IDEpICU+JSANCiAgc2V0X2VuZ2luZSgiZ2xtbmV0IikNCg0Kc2VsbHNfd2Zsb3cgPC0gDQogIHdvcmtmbG93KCkgJT4lIA0KICBhZGRfcmVjaXBlKHNlbGxzX3JlYykgDQoNCnJmX3dmbG93IDwtIA0KICBzZWxsc193ZmxvdyAlPiUgDQogIGFkZF9tb2RlbChyZl9tb2QpDQoNCg0KbGFzc29fd2Zsb3cgPC0gDQogIHNlbGxzX3dmbG93ICU+JSANCiAgYWRkX21vZGVsKGxhc3NvX21vZCkNCg0KcmZfd2Zsb3cNCmxhc3NvX3dmbG93DQpgYGANCg0KIyMjIEV2YWx1YXRpb24NCg0KV2UgY2FuIG5vdyBmaXQgdGhlIHRyYWluIGRhdGEgYW5kIGNoZWNrIHRoZSByZXN1bHRzIHdpdGggb3VyIHR3byBkaWZmZXJlbnQgbW9kZWxzLg0KDQpPbmUgZm9yIHJhbmRvbSBmb3Jlc3Q6DQoNCmBgYHtyIGZpdF9yZl9tb2RlbCB9DQpzZWxsc19maXQgPC0gDQogIHJmX3dmbG93ICU+JSANCiAgZml0KGRhdGEgPSBkZl90cmFpbikNCg0Kc2VsbHNfcmZfYXVnIDwtIA0KICBhdWdtZW50KHNlbGxzX2ZpdCwgdGVzdF9kYXRhKQ0KDQpzZWxsc19yZl9hdWcgJT4lIA0KICBzZWxlY3QobmFtZSwgdW5pdF9wcmljZV9nb2xkX3NlbGxzLCAucHJlZCkNCg0Kc2VsbHNfcmZfYXVnICU+JSANCiAgbWV0cmljcyh0cnV0aCA9IHVuaXRfcHJpY2VfZ29sZF9zZWxscywgZXN0aW1hdGUgPSAucHJlZCkNCmBgYA0KDQpPbmUgZm9yIGxhc3NvOg0KDQpgYGB7ciBmaXRfbGFzc29fbW9kZWwgfQ0Kc2VsbHNfZml0IDwtIA0KICBsYXNzb193ZmxvdyAlPiUgDQogIGZpdChkYXRhID0gZGZfdHJhaW4pDQoNCnNlbGxzX2xhc3NvX2F1ZyA8LSANCiAgYXVnbWVudChzZWxsc19maXQsIHRlc3RfZGF0YSkNCg0Kc2VsbHNfbGFzc29fYXVnICU+JSANCiAgc2VsZWN0KG5hbWUsIHVuaXRfcHJpY2VfZ29sZF9zZWxscywgLnByZWQpDQoNCnNlbGxzX2xhc3NvX2F1ZyAlPiUgDQogIG1ldHJpY3ModHJ1dGggPSB1bml0X3ByaWNlX2dvbGRfc2VsbHMsIGVzdGltYXRlID0gLnByZWQpDQpgYGANCg0KQ29tcGFyaW5nIHRoZSBlcnJvcnMgb2YgdGhvc2UgdHdvIGVuZ2luZXMsIHRoZSByYW5kb20gZm9yZXN0IGFsZ29yaXRobSBoYXMgYSBtdWNoIGhpZ2hlciByb290IG1lYW4gc3F1YXJlIGVycm9yIGFuZCBtZWFuIGFic29sdXRlIGVycm9yLg0KQm90aCBtb2RlbHMgYXJlIG92ZXJ3aGVsbWluZ2x5IGV4YWN0LCByYW5kb20gZm9yZXN0IG9uIDk0JSBvZiB0aGUgdGltZSBhbmQgbGFzc28gb24gOTksOTg4JS4NCg0KYGBge3J9DQpzZWxsc19maXQgJT4lDQogIGV4dHJhY3RfZml0X3BhcnNuaXAoKSAlPiUNCiAgdGlkeSgpDQpgYGANCg0KTm93IHdlIGhhdmUgaXQgaW4gbnVtYmVycyB0aGF0IHRoZSBvbmx5IGZlYXR1cmUgdGhhdCBoYXMgaW5mbHVlbmNlIG9uIG91ciBwcmVkaWN0aW9uIGlzIHRoZSBidXlpbmcgcHJpY2UuDQpNYXliZSB3ZSBjYW4gY2hhbmdlIHRoaXMgdXNpbmcgaHlwZXIgcGFyYW1ldGVyIHR1bmluZy4NCg0KIyMjIFR1bmluZw0KDQpgYGB7cn0NCnNldC5zZWVkKDEyMzQpDQoNCnNlbGxzX2Jvb3QgPC0gYm9vdHN0cmFwcyhkZl90cmFpbikNCg0KdHVuZV9zcGVjIDwtIGxpbmVhcl9yZWcocGVuYWx0eSA9IHR1bmUoKSwgbWl4dHVyZSA9IDEpICU+JQ0KICBzZXRfZW5naW5lKCJnbG1uZXQiKQ0KDQpsYW1iZGFfZ3JpZCA8LSBncmlkX3JlZ3VsYXIocGVuYWx0eSgpLCBsZXZlbHMgPSA1MCkNCmBgYA0KDQpgYGB7cn0NCmRvUGFyYWxsZWw6OnJlZ2lzdGVyRG9QYXJhbGxlbCgpDQoNCnNldC5zZWVkKDIwMjApDQoNCmxhc3NvX2dyaWQgPC0gDQogIHR1bmVfZ3JpZCgNCiAgc2VsbHNfd2Zsb3cgJT4lIA0KICAgIGFkZF9tb2RlbCh0dW5lX3NwZWMpLA0KICAgIHJlc2FtcGxlcyA9IHNlbGxzX2Jvb3QsDQogICAgZ3JpZCA9IGxhbWJkYV9ncmlkDQopDQoNCmxhc3NvX2dyaWQgJT4lDQogIGNvbGxlY3RfbWV0cmljcygpDQpgYGANCg0KYGBge3J9DQpsYXNzb19ncmlkICU+JQ0KICBjb2xsZWN0X21ldHJpY3MoKSAlPiUNCiAgZ2dwbG90KGFlcyhwZW5hbHR5LCBtZWFuLCBjb2xvciA9IC5tZXRyaWMpKSArDQogIGdlb21fZXJyb3JiYXIoYWVzKHltaW4gPSBtZWFuIC0gc3RkX2VyciwNCiAgICAgICAgICAgICAgICAgICAgeW1heCA9IG1lYW4gKyBzdGRfZXJyKSwNCiAgICAgICAgICAgICAgICBhbHBoYSA9IDAuNSkgKw0KICBnZW9tX2xpbmUoc2l6ZSA9IDEuNSkgKw0KICBmYWNldF93cmFwKH4ubWV0cmljLCBzY2FsZXMgPSAiZnJlZSIsIG5yb3cgPSAyKSArDQogIHNjYWxlX3hfbG9nMTAoKSArDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikNCmBgYA0KDQpXZWxsIGJhZCBsdWNrLCBub3RoaW5nIHRvIGdhaW4gaGVyZS4gVGhlIHJtc2UgZXZlbiByYWlzZXMgYWZ0ZXIgdG9vIG11Y2ggdHVuaW5nLg0KDQpOb25ldGhlbGVzcywgdGhlIGVycm9yIHZhbHVlcyBhcmUgYWxyZWFkeSBwcmV0dHkgbG93Lg0KDQpgYGB7ciB2aXN1YWxpemVfbW9kZWwgfQ0KDQpzZWxsc19qb2luX2F1ZyA8LSBzZWxsc19sYXNzb19hdWcgJT4lIA0KICBzZWxlY3QoaWQgPSBpZCwgbGFzc29fcHJlZCA9IC5wcmVkLCB1bml0X3ByaWNlX2dvbGRfc2VsbHMpICU+JSANCiAgbGVmdF9qb2luKHNlbGxzX3JmX2F1ZyAlPiUgc2VsZWN0KGlkID0gaWQsIHJmX3ByZWQgPSAucHJlZCksIGJ5ID0gImlkIikNCg0Kc2VsbHNfam9pbl9hdWcgJT4lICANCiAgZ2dwbG90KCkgKw0KICBnZW9tX3BvaW50KGFlcyh4ID0gcmZfcHJlZCwgeSA9IHVuaXRfcHJpY2VfZ29sZF9zZWxscyksIGNvbG9yID0gIiMwNTU0MWEiKSArDQogIGdlb21fcG9pbnQoYWVzKHggPSBsYXNzb19wcmVkLCB5ID0gdW5pdF9wcmljZV9nb2xkX3NlbGxzKSwgY29sb3IgPSAiIzA0MDU1MiIpICsNCiAgZ2VvbV9hYmxpbmUoY29sID0gInJlZCIsIGx0eSA9IDIpICsNCiAgbGFicyh4ID0gIlByZWRpY3Rpb24iLCB5ID0gIkdvbGQgdmFsdWUiLA0KICAgICAgIHRpdGxlID0gIkl0ZW0gcHJpY2UgcHJlZGljdGlvbnMiLCBzdWJ0aXRsZSA9ICJVc2luZyByYW5kb20gZm9yZXN0IGFuZCBsYXNzbyByZWdyZXNzaW9ucyIsDQogICAgICAgY29sb3IgPSBjKCJBIiwgIkIiKSwNCiAgICAgICBjYXB0aW9uID0gcGFzdGUoIkRhdGEgZnJvbSIsIHBhcmFtcyRkYXRhX2RhdGUpKQ0KYGBgDQoNCldoYXQgaXMgaW50ZXJlc3RpbmcgZm9yIHVzIGFyZSB0aGUgcHJpY2VzIHRoYXQgYXJlIGJsb3cgdGhlIHByZWRpY3Rpb24gbGluZSwgbWVhbmluZyB0aGF0IHRoZXJlIG1heSBiZSBhIGN1cnJlbnQgbG93IHRoYXQgd2lsbCBwcm9iYWJseSBjaGFuZ2UgaW4gdGhlIGZ1dHVyZS4NCg0KIyMjIEV2YWx1YXRlIG9uIHRlc3Qgc2V0DQoNCkRvIHRoZSBsYXN0IGZpdCBvbiB0aGUgdGVzdCBkYXRhLiBXZSBjaGFuZ2VkIG5vIHBhcmFtZXRlciBzbyB0aGUgb3V0cHV0IHNob3VsZCBiZSB0aGUgc2FtZSBhcyBiZWZvcmUuDQoNCmBgYHtyfQ0KIyBmaW5hbCBldmFsdWF0aW9uIHdpdGggdGVzdCBkYXRhDQpsYXN0X2ZpdF9sYXNzbyA8LSBsYXN0X2ZpdChsYXNzb193Zmxvdywgc3BsaXQgPSBkYXRhX3NwbGl0KQ0KDQojIHNob3cgUk1TRSBhbmQgUlNRDQpsYXN0X2ZpdF9sYXNzbyAlPiUgDQogIGNvbGxlY3RfbWV0cmljcygpDQpgYGA=